dahua_sdk.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510
  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. """
  158. # === 必需函数 ===
  159. # CLIENT_Init(BOOL (CALLBACK *cbDisConnect), LDWORD)
  160. self.sdk.CLIENT_Init.argtypes = [ctypes.c_void_p, c_long]
  161. self.sdk.CLIENT_Init.restype = ctypes.c_bool
  162. # CLIENT_Cleanup()
  163. self.sdk.CLIENT_Cleanup.argtypes = []
  164. self.sdk.CLIENT_Cleanup.restype = None
  165. # CLIENT_LoginWithHighLevelSecurity -> LLONG
  166. self.sdk.CLIENT_LoginWithHighLevelSecurity.argtypes = [
  167. POINTER(self.NET_IN_LOGIN),
  168. POINTER(self.NET_OUT_LOGIN)
  169. ]
  170. self.sdk.CLIENT_LoginWithHighLevelSecurity.restype = c_long
  171. # CLIENT_Logout(LLONG)
  172. self.sdk.CLIENT_Logout.argtypes = [c_long]
  173. self.sdk.CLIENT_Logout.restype = ctypes.c_bool
  174. # CLIENT_RealPlay(LLONG, int, void*) -> LLONG
  175. self.sdk.CLIENT_RealPlay.argtypes = [c_long, c_int, ctypes.c_void_p]
  176. self.sdk.CLIENT_RealPlay.restype = c_long
  177. # CLIENT_StopRealPlay(LLONG) -> BOOL
  178. self.sdk.CLIENT_StopRealPlay.argtypes = [c_long]
  179. self.sdk.CLIENT_StopRealPlay.restype = ctypes.c_bool
  180. # CLIENT_DHPTZControlEx(LLONG, int, DWORD, LONG, LONG, LONG, BOOL)
  181. self.sdk.CLIENT_DHPTZControlEx.argtypes = [
  182. c_long, c_int, c_uint32, c_int, c_int, c_int, ctypes.c_bool
  183. ]
  184. self.sdk.CLIENT_DHPTZControlEx.restype = ctypes.c_bool
  185. # CLIENT_SnapPicture(LLONG, SNAP_PARAMS*) -> BOOL
  186. self.sdk.CLIENT_SnapPicture.argtypes = [c_long, POINTER(self.SNAP_PARAMS)]
  187. self.sdk.CLIENT_SnapPicture.restype = ctypes.c_bool
  188. # CLIENT_GetLastError() -> DWORD
  189. self.sdk.CLIENT_GetLastError.argtypes = []
  190. self.sdk.CLIENT_GetLastError.restype = c_uint32
  191. # === 可选函数(某些SDK版本/平台可能缺失)===
  192. self._optional_funcs = {}
  193. self._bind_optional('CLIENT_SetSnapRevCallBack', [ctypes.c_void_p, c_long], None)
  194. self._bind_optional('CLIENT_RealPlayEx', [c_long, c_int, ctypes.c_void_p, c_int], c_long)
  195. self._bind_optional('CLIENT_StopRealPlayEx', [c_long], ctypes.c_bool)
  196. self._bind_optional('CLIENT_SetVideoProcCallBack', [ctypes.c_void_p, c_long], None)
  197. def _bind_optional(self, name: str, argtypes: list, restype):
  198. """绑定可选SDK函数,缺失时设为None而不报错"""
  199. try:
  200. func = getattr(self.sdk, name, None)
  201. if func is not None:
  202. func.argtypes = argtypes
  203. func.restype = restype
  204. self._optional_funcs[name] = func
  205. else:
  206. self._optional_funcs[name] = None
  207. print(f"SDK可选函数缺失: {name}")
  208. except (AttributeError, OSError) as e:
  209. self._optional_funcs[name] = None
  210. print(f"SDK可选函数不可用: {name} - {e}")
  211. def _has_func(self, name: str) -> bool:
  212. """检查SDK函数是否可用"""
  213. return self._optional_funcs.get(name) is not None
  214. def init(self, disconnect_callback: Callable = None) -> bool:
  215. """
  216. 初始化SDK
  217. Args:
  218. disconnect_callback: 断线回调函数
  219. Returns:
  220. 是否成功
  221. """
  222. if not self.sdk:
  223. return False
  224. if disconnect_callback:
  225. # 保存回调引用,防止被垃圾回收
  226. self._disconnect_callback = ctypes.CFUNCTYPE(
  227. ctypes.c_bool, c_long, c_char_p, c_int, c_long
  228. )(disconnect_callback)
  229. result = self.sdk.CLIENT_Init(self._disconnect_callback, 0)
  230. self.initialized = result
  231. return result
  232. def cleanup(self):
  233. """清理SDK资源"""
  234. if self.sdk and self.initialized:
  235. self.sdk.CLIENT_Cleanup()
  236. self.initialized = False
  237. def login(self, ip: str, port: int, username: str, password: str) -> Tuple[Optional[int], int]:
  238. """
  239. 登录设备
  240. Args:
  241. ip: 设备IP
  242. port: 端口号
  243. username: 用户名
  244. password: 密码
  245. Returns:
  246. (登录句柄, 错误码) - 成功时错误码为0
  247. """
  248. if not self.sdk:
  249. return None, -1
  250. in_param = self.NET_IN_LOGIN()
  251. in_param.dwSize = ctypes.sizeof(self.NET_IN_LOGIN)
  252. in_param.szIP = ip.encode('utf-8')
  253. in_param.nPort = port
  254. in_param.szUserName = username.encode('utf-8')
  255. in_param.szPassword = password.encode('utf-8')
  256. in_param.emSpecCap = 0 # EM_LOGIN_SPEC_CAP_TCP
  257. out_param = self.NET_OUT_LOGIN()
  258. out_param.dwSize = ctypes.sizeof(self.NET_OUT_LOGIN)
  259. login_handle = self.sdk.CLIENT_LoginWithHighLevelSecurity(
  260. byref(in_param), byref(out_param)
  261. )
  262. if login_handle == 0:
  263. return None, out_param.nError
  264. return login_handle, 0
  265. def logout(self, login_handle: int) -> bool:
  266. """登出设备"""
  267. if not self.sdk or login_handle <= 0:
  268. return False
  269. return self.sdk.CLIENT_Logout(login_handle)
  270. def real_play(self, login_handle: int, channel: int = 0) -> Optional[int]:
  271. """
  272. 开始实时预览
  273. Args:
  274. login_handle: 登录句柄
  275. channel: 通道号
  276. Returns:
  277. 预览句柄
  278. """
  279. if not self.sdk or login_handle <= 0:
  280. return None
  281. play_handle = self.sdk.CLIENT_RealPlay(login_handle, channel, None)
  282. return play_handle if play_handle > 0 else None
  283. def real_play_ex(self, login_handle: int, channel: int = 0,
  284. use_callback: bool = True) -> Optional[int]:
  285. if not self.sdk or login_handle <= 0:
  286. return None
  287. if not self._has_func('CLIENT_RealPlayEx'):
  288. return self.real_play(login_handle, channel)
  289. if use_callback and self._has_func('CLIENT_SetVideoProcCallBack'):
  290. if channel not in self._video_frame_buffers:
  291. self._video_frame_buffers[channel] = VideoFrameBuffer()
  292. if self._video_callback is None:
  293. self._setup_video_callback()
  294. play_handle = self.sdk.CLIENT_RealPlayEx(login_handle, channel, None, 0)
  295. else:
  296. play_handle = self.sdk.CLIENT_RealPlayEx(login_handle, channel, None, 0) if self._has_func('CLIENT_RealPlayEx') else self.real_play(login_handle, channel)
  297. return play_handle if play_handle > 0 else None
  298. def _setup_video_callback(self):
  299. if not self._has_func('CLIENT_SetVideoProcCallBack'):
  300. print("SDK不支持CLIENT_SetVideoProcCallBack,视频回调不可用")
  301. return
  302. def video_callback(real_handle: c_long, data_type: c_uint32,
  303. buffer: POINTER(c_ubyte), buf_size: c_uint32,
  304. param: c_long, user_data: c_long):
  305. if data_type == 1 and buf_size > 0:
  306. pass
  307. self._video_callback = CFUNCTYPE(
  308. None, c_long, c_uint32, POINTER(c_ubyte), c_uint32, c_long, c_long
  309. )(video_callback)
  310. self.sdk.CLIENT_SetVideoProcCallBack(self._video_callback, 0)
  311. def get_video_frame_buffer(self, channel: int = 0) -> Optional[VideoFrameBuffer]:
  312. """获取视频帧缓冲区"""
  313. return self._video_frame_buffers.get(channel)
  314. def stop_real_play(self, play_handle: int) -> bool:
  315. """停止实时预览"""
  316. if not self.sdk or play_handle <= 0:
  317. return False
  318. return self.sdk.CLIENT_StopRealPlay(play_handle)
  319. def ptz_control(self, login_handle: int, channel: int,
  320. command: int, param1: int, param2: int, param3: int,
  321. stop: bool = False) -> bool:
  322. """
  323. PTZ控制
  324. Args:
  325. login_handle: 登录句柄
  326. channel: 通道号
  327. command: 控制命令 (DH_EXTPTZ_ControlType)
  328. param1-param3: 控制参数
  329. stop: 是否停止(用于持续移动)
  330. Returns:
  331. 是否成功
  332. """
  333. if not self.sdk or login_handle <= 0:
  334. print(f"[PTZ] 失败: sdk={self.sdk is not None}, handle={login_handle}")
  335. return False
  336. # 命令名称映射 (用于调试日志)
  337. cmd_names = {
  338. 0: 'UP', 1: 'DOWN', 2: 'LEFT', 3: 'RIGHT',
  339. 4: 'ZOOM_ADD', 5: 'ZOOM_DEC',
  340. 0x20: 'LEFTTOP', 0x21: 'RIGHTTOP', 0x22: 'LEFTDOWN', 0x23: 'RIGHTDOWN',
  341. 0x33: 'FASTGOTO', 0x43: 'EXACTGOTO', 0x44: 'RESETZERO',
  342. 0x45: 'MOVE_ABSOLUTELY', 0x46: 'MOVE_CONTINUOUSLY', 0x47: 'GOTOPRESET',
  343. }
  344. cmd_name = cmd_names.get(command, f'CMD_{command:#x}')
  345. result = self.sdk.CLIENT_DHPTZControlEx(
  346. login_handle, channel, command, param1, param2, param3, stop
  347. )
  348. print(f"[PTZ] {cmd_name}(ch={channel}, p1={param1}, p2={param2}, p3={param3}, stop={stop}) → {'✓' if result else '✗'}")
  349. if not result:
  350. # 获取SDK错误码
  351. error = self.sdk.CLIENT_GetLastError() if hasattr(self.sdk, 'CLIENT_GetLastError') else -1
  352. print(f"[PTZ] 错误码: {error}")
  353. return result
  354. def snap_picture(self, login_handle: int, channel: int = 0,
  355. cmd_serial: int = 0) -> bool:
  356. """
  357. 抓拍图片
  358. Args:
  359. login_handle: 登录句柄
  360. channel: 通道号
  361. cmd_serial: 命令序列号
  362. Returns:
  363. 是否成功
  364. """
  365. if not self.sdk or login_handle <= 0:
  366. return False
  367. params = self.SNAP_PARAMS()
  368. params.Channel = channel
  369. params.mode = 0 # SNAP_TYP_TIMING
  370. params.CmdSerial = cmd_serial
  371. params.PicTransType = 0
  372. params.Quality = 1
  373. params.PicFormat = 0 # BMP
  374. return self.sdk.CLIENT_SnapPicture(login_handle, byref(params))
  375. # PTZ控制命令常量 (从dhnetsdk.h提取)
  376. class PTZCommand:
  377. """PTZ控制命令常量 (DH_EXTPTZ_ControlType from dhnetsdk.h)"""
  378. # 基本控制 (DH_PTZ_ControlType)
  379. UP = 0
  380. DOWN = 1
  381. LEFT = 2
  382. RIGHT = 3
  383. ZOOM_ADD = 4
  384. ZOOM_DEC = 5
  385. FOCUS_ADD = 6
  386. FOCUS_DEC = 7
  387. APERTURE_ADD = 8
  388. APERTURE_DEC = 9
  389. # 扩展控制 (DH_EXTPTZ_ControlType, 从0x20开始)
  390. LEFTTOP = 0x20
  391. RIGHTTOP = 0x21
  392. LEFTDOWN = 0x22
  393. RIGHTDOWN = 0x23
  394. ADDTOLOOP = 0x24
  395. DELFROMLOOP = 0x25
  396. CLOSELOOP = 0x26
  397. STARTPANCRUISE = 0x27
  398. STOPPANCRUISE = 0x28
  399. SETLEFTBORDER = 0x29
  400. SETRIGHTBORDER = 0x2a
  401. STARTLINESCAN = 0x2b
  402. CLOSELINESCAN = 0x2c
  403. SETMODESTART = 0x2d
  404. SETMODESTOP = 0x2e
  405. RUNMODE = 0x2f
  406. STOPMODE = 0x30
  407. DELETEMODE = 0x31
  408. REVERSECOMM = 0x32
  409. FASTGOTO = 0x33
  410. AUXIOPEN = 0x34
  411. AUXICLOSE = 0x35
  412. OPENMENU = 0x36
  413. CLOSEMENU = 0x37
  414. MENUOK = 0x38
  415. MENUCANCEL = 0x39
  416. MENUUP = 0x3a
  417. MENUDOWN = 0x3b
  418. MENULEFT = 0x3c
  419. MENURIGHT = 0x3d
  420. ALARMHANDLE = 0x40
  421. MATRIXSWITCH = 0x41
  422. LIGHTCONTROL = 0x42
  423. EXACTGOTO = 0x43 # 三维精确定位
  424. RESETZERO = 0x44
  425. MOVE_ABSOLUTELY = 0x45
  426. MOVE_CONTINUOUSLY = 0x46
  427. GOTOPRESET = 0x47
  428. SET_VIEW_RANGE = 0x49
  429. FOCUS_ABSOLUTELY = 0x4a
  430. class SDKError:
  431. """SDK错误码"""
  432. SUCCESS = 0
  433. NET_ERROR_PASSWORD = 1
  434. NET_ERROR_USER = 2
  435. NET_ERROR_TIMEOUT = 3
  436. NET_ERROR_RECONNECT = 4