dahua_sdk.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426
  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_ulong, c_char_p, c_ubyte, POINTER, Structure, byref, CFUNCTYPE
  10. from typing import Optional, Callable, Tuple, Dict, Any
  11. class VideoFrameBuffer:
  12. """
  13. 视频帧缓冲区
  14. 用于SDK回调存储帧数据
  15. """
  16. def __init__(self, max_size: int = 10):
  17. self.buffer = queue.Queue(maxsize=max_size)
  18. self.lock = threading.Lock()
  19. self.latest_frame = None
  20. def put(self, frame_data: bytes, width: int, height: int, frame_type: int = 0):
  21. """存入帧数据"""
  22. with self.lock:
  23. self.latest_frame = {
  24. 'data': frame_data,
  25. 'width': width,
  26. 'height': height,
  27. 'type': frame_type
  28. }
  29. try:
  30. self.buffer.put_nowait(self.latest_frame)
  31. except queue.Full:
  32. pass
  33. def get(self, timeout: float = 0.1) -> Optional[Dict[str, Any]]:
  34. """获取帧数据"""
  35. try:
  36. return self.buffer.get(timeout=timeout)
  37. except queue.Empty:
  38. return None
  39. def get_latest(self) -> Optional[Dict[str, Any]]:
  40. """获取最新帧数据"""
  41. with self.lock:
  42. return self.latest_frame
  43. # 加载SDK库
  44. class DahuaSDK:
  45. """大华SDK封装类"""
  46. def __init__(self, lib_path: str):
  47. """
  48. 初始化SDK
  49. Args:
  50. lib_path: SDK库文件路径
  51. """
  52. self.lib_path = lib_path
  53. self.sdk = None
  54. self.initialized = False
  55. self._disconnect_callback = None # 保存回调引用,防止被垃圾回收
  56. # 视频回调相关
  57. self._video_callback = None
  58. self._video_frame_buffers: Dict[int, VideoFrameBuffer] = {}
  59. self._setup_structures()
  60. self._load_library()
  61. def _setup_structures(self):
  62. """设置SDK所需的C结构体"""
  63. # 登录输入参数
  64. class NET_IN_LOGIN_WITH_HIGHLEVEL_SECURITY(Structure):
  65. _fields_ = [
  66. ('dwSize', c_ulong),
  67. ('szIP', ctypes.c_char * 64),
  68. ('nPort', c_int),
  69. ('szUserName', ctypes.c_char * 64),
  70. ('szPassword', ctypes.c_char * 64),
  71. ('emSpecCap', c_int),
  72. ]
  73. # 登录输出参数
  74. class NET_OUT_LOGIN_WITH_HIGHLEVEL_SECURITY(Structure):
  75. _fields_ = [
  76. ('dwSize', c_ulong),
  77. ('pstuDeviceInfo', ctypes.c_void_p),
  78. ('nError', c_int),
  79. ('byRes', ctypes.c_char * 128),
  80. ]
  81. # 抓拍参数
  82. class SNAP_PARAMS(Structure):
  83. _fields_ = [
  84. ('Channel', c_int),
  85. ('mode', c_int),
  86. ('CmdSerial', c_int),
  87. ('PicTransType', c_int),
  88. ('SendTotal', c_int),
  89. ('Quality', c_int),
  90. ('PicFormat', c_int),
  91. ('PicWidth', c_int),
  92. ('PicHeight', c_int),
  93. ('SnapDelayTime', c_int),
  94. ('byRes', ctypes.c_char * 12),
  95. ]
  96. self.NET_IN_LOGIN = NET_IN_LOGIN_WITH_HIGHLEVEL_SECURITY
  97. self.NET_OUT_LOGIN = NET_OUT_LOGIN_WITH_HIGHLEVEL_SECURITY
  98. self.SNAP_PARAMS = SNAP_PARAMS
  99. def _load_library(self):
  100. """加载SDK动态库"""
  101. try:
  102. lib_dir = os.path.dirname(self.lib_path)
  103. # 预加载依赖库 (确保库依赖能正确解析)
  104. # 大华SDK依赖 libcrypto.so, libssl.so 等
  105. if lib_dir:
  106. # 使用 RTLD_GLOBAL 标志加载依赖库,使其符号对后续库可见
  107. try:
  108. # 尝试预加载常见的依赖库
  109. for dep_lib in ['libcrypto.so', 'libssl.so']:
  110. dep_path = os.path.join(lib_dir, dep_lib)
  111. if os.path.exists(dep_path):
  112. ctypes.CDLL(dep_path, mode=ctypes.RTLD_GLOBAL)
  113. except Exception as e:
  114. # 依赖库加载失败不一定是致命错误,继续尝试加载主库
  115. print(f"加载依赖库警告: {e}")
  116. # 加载主SDK库
  117. self.sdk = ctypes.CDLL(self.lib_path)
  118. self._setup_functions()
  119. self.initialized = True
  120. print(f"成功加载大华SDK: {self.lib_path}")
  121. except Exception as e:
  122. print(f"加载SDK失败: {e}")
  123. raise
  124. def _setup_functions(self):
  125. """设置SDK函数签名"""
  126. # CLIENT_Init
  127. self.sdk.CLIENT_Init.argtypes = [ctypes.c_void_p, c_ulong]
  128. self.sdk.CLIENT_Init.restype = ctypes.c_bool
  129. # CLIENT_Cleanup
  130. self.sdk.CLIENT_Cleanup.argtypes = []
  131. self.sdk.CLIENT_Cleanup.restype = None
  132. # CLIENT_LoginWithHighLevelSecurity
  133. self.sdk.CLIENT_LoginWithHighLevelSecurity.argtypes = [
  134. POINTER(self.NET_IN_LOGIN),
  135. POINTER(self.NET_OUT_LOGIN)
  136. ]
  137. self.sdk.CLIENT_LoginWithHighLevelSecurity.restype = c_long
  138. # CLIENT_Logout
  139. self.sdk.CLIENT_Logout.argtypes = [c_long]
  140. self.sdk.CLIENT_Logout.restype = ctypes.c_bool
  141. # CLIENT_RealPlay
  142. self.sdk.CLIENT_RealPlay.argtypes = [c_long, c_int, ctypes.c_void_p]
  143. self.sdk.CLIENT_RealPlay.restype = c_long
  144. # CLIENT_StopRealPlay
  145. self.sdk.CLIENT_StopRealPlay.argtypes = [c_long]
  146. self.sdk.CLIENT_StopRealPlay.restype = ctypes.c_bool
  147. # CLIENT_DHPTZControlEx
  148. self.sdk.CLIENT_DHPTZControlEx.argtypes = [
  149. c_long, c_int, c_ulong, c_long, c_long, c_long, ctypes.c_bool
  150. ]
  151. self.sdk.CLIENT_DHPTZControlEx.restype = ctypes.c_bool
  152. # CLIENT_SnapPicture
  153. self.sdk.CLIENT_SnapPicture.argtypes = [c_long, POINTER(self.SNAP_PARAMS)]
  154. self.sdk.CLIENT_SnapPicture.restype = ctypes.c_bool
  155. # CLIENT_SetSnapRevCallBack
  156. self.sdk.CLIENT_SetSnapRevCallBack.argtypes = [ctypes.c_void_p, c_ulong]
  157. self.sdk.CLIENT_SetSnapRevCallBack.restype = None
  158. # CLIENT_RealPlayEx - 扩展实时预览 (支持回调)
  159. self.sdk.CLIENT_RealPlayEx.argtypes = [c_long, c_int, ctypes.c_void_p, c_int]
  160. self.sdk.CLIENT_RealPlayEx.restype = c_long
  161. # CLIENT_StopRealPlayEx
  162. self.sdk.CLIENT_StopRealPlayEx.argtypes = [c_long]
  163. self.sdk.CLIENT_StopRealPlayEx.restype = ctypes.c_bool
  164. # CLIENT_SetVideoProcCallBack - 设置视频回调
  165. self.sdk.CLIENT_SetVideoProcCallBack.argtypes = [ctypes.c_void_p, c_ulong]
  166. self.sdk.CLIENT_SetVideoProcCallBack.restype = None
  167. def init(self, disconnect_callback: Callable = None) -> bool:
  168. """
  169. 初始化SDK
  170. Args:
  171. disconnect_callback: 断线回调函数
  172. Returns:
  173. 是否成功
  174. """
  175. if not self.sdk:
  176. return False
  177. if disconnect_callback:
  178. # 保存回调引用,防止被垃圾回收
  179. self._disconnect_callback = ctypes.CFUNCTYPE(
  180. None, c_long, c_char_p, c_int, c_ulong
  181. )(disconnect_callback)
  182. result = self.sdk.CLIENT_Init(self._disconnect_callback, 0)
  183. self.initialized = result
  184. return result
  185. def cleanup(self):
  186. """清理SDK资源"""
  187. if self.sdk and self.initialized:
  188. self.sdk.CLIENT_Cleanup()
  189. self.initialized = False
  190. def login(self, ip: str, port: int, username: str, password: str) -> Tuple[Optional[int], int]:
  191. """
  192. 登录设备
  193. Args:
  194. ip: 设备IP
  195. port: 端口号
  196. username: 用户名
  197. password: 密码
  198. Returns:
  199. (登录句柄, 错误码) - 成功时错误码为0
  200. """
  201. if not self.sdk:
  202. return None, -1
  203. in_param = self.NET_IN_LOGIN()
  204. in_param.dwSize = ctypes.sizeof(self.NET_IN_LOGIN)
  205. in_param.szIP = ip.encode('utf-8')
  206. in_param.nPort = port
  207. in_param.szUserName = username.encode('utf-8')
  208. in_param.szPassword = password.encode('utf-8')
  209. in_param.emSpecCap = 0 # EM_LOGIN_SPEC_CAP_TCP
  210. out_param = self.NET_OUT_LOGIN()
  211. out_param.dwSize = ctypes.sizeof(self.NET_OUT_LOGIN)
  212. login_handle = self.sdk.CLIENT_LoginWithHighLevelSecurity(
  213. byref(in_param), byref(out_param)
  214. )
  215. if login_handle == 0:
  216. return None, out_param.nError
  217. return login_handle, 0
  218. def logout(self, login_handle: int) -> bool:
  219. """登出设备"""
  220. if not self.sdk or login_handle <= 0:
  221. return False
  222. return self.sdk.CLIENT_Logout(login_handle)
  223. def real_play(self, login_handle: int, channel: int = 0) -> Optional[int]:
  224. """
  225. 开始实时预览
  226. Args:
  227. login_handle: 登录句柄
  228. channel: 通道号
  229. Returns:
  230. 预览句柄
  231. """
  232. if not self.sdk or login_handle <= 0:
  233. return None
  234. play_handle = self.sdk.CLIENT_RealPlay(login_handle, channel, None)
  235. return play_handle if play_handle > 0 else None
  236. def real_play_ex(self, login_handle: int, channel: int = 0,
  237. use_callback: bool = True) -> Optional[int]:
  238. """
  239. 开始实时预览 (扩展版,支持视频回调)
  240. Args:
  241. login_handle: 登录句柄
  242. channel: 通道号
  243. use_callback: 是否使用回调方式获取视频
  244. Returns:
  245. 预览句柄
  246. """
  247. if not self.sdk or login_handle <= 0:
  248. return None
  249. if use_callback:
  250. # 创建帧缓冲区
  251. if channel not in self._video_frame_buffers:
  252. self._video_frame_buffers[channel] = VideoFrameBuffer()
  253. # 设置视频回调
  254. if self._video_callback is None:
  255. self._setup_video_callback()
  256. # 使用 RealPlayEx 启动预览 (rType=0 表示标准预览)
  257. play_handle = self.sdk.CLIENT_RealPlayEx(login_handle, channel, None, 0)
  258. else:
  259. play_handle = self.sdk.CLIENT_RealPlay(login_handle, channel, None)
  260. return play_handle if play_handle > 0 else None
  261. def _setup_video_callback(self):
  262. """设置视频数据回调函数"""
  263. # 视频回调函数签名:
  264. # void CALLBACK RealDataCallBackEx(LONG lRealHandle, DWORD dwDataType, BYTE *pBuffer, DWORD dwBufSize, LONG param, LDWORD dwUser)
  265. def video_callback(real_handle: c_long, data_type: c_ulong,
  266. buffer: POINTER(c_ubyte), buf_size: c_ulong,
  267. param: c_long, user_data: c_ulong):
  268. """视频数据回调 (内部函数)"""
  269. # data_type:
  270. # 0 = 系统头
  271. # 1 = 流数据 (P帧/I帧)
  272. # 2 = 音频数据
  273. # 3 = 智能信息
  274. if data_type == 1 and buf_size > 0: # 视频流数据
  275. # 注意: 这里收到的是编码后的视频流数据,需要解码才能得到图像
  276. # 实际解码需要使用 ffmpeg 或 SDK 的解码接口
  277. pass
  278. # 创建 ctypes 回调并保存引用
  279. self._video_callback = CFUNCTYPE(
  280. None, c_long, c_ulong, POINTER(c_ubyte), c_ulong, c_long, c_ulong
  281. )(video_callback)
  282. # 设置 SDK 回调
  283. self.sdk.CLIENT_SetVideoProcCallBack(self._video_callback, 0)
  284. def get_video_frame_buffer(self, channel: int = 0) -> Optional[VideoFrameBuffer]:
  285. """获取视频帧缓冲区"""
  286. return self._video_frame_buffers.get(channel)
  287. def stop_real_play(self, play_handle: int) -> bool:
  288. """停止实时预览"""
  289. if not self.sdk or play_handle <= 0:
  290. return False
  291. return self.sdk.CLIENT_StopRealPlay(play_handle)
  292. def ptz_control(self, login_handle: int, channel: int,
  293. command: int, param1: int, param2: int, param3: int,
  294. stop: bool = False) -> bool:
  295. """
  296. PTZ控制
  297. Args:
  298. login_handle: 登录句柄
  299. channel: 通道号
  300. command: 控制命令 (DH_EXTPTZ_ControlType)
  301. param1-param3: 控制参数
  302. stop: 是否停止(用于持续移动)
  303. Returns:
  304. 是否成功
  305. """
  306. if not self.sdk or login_handle <= 0:
  307. return False
  308. return self.sdk.CLIENT_DHPTZControlEx(
  309. login_handle, channel, command, param1, param2, param3, stop
  310. )
  311. def snap_picture(self, login_handle: int, channel: int = 0,
  312. cmd_serial: int = 0) -> bool:
  313. """
  314. 抓拍图片
  315. Args:
  316. login_handle: 登录句柄
  317. channel: 通道号
  318. cmd_serial: 命令序列号
  319. Returns:
  320. 是否成功
  321. """
  322. if not self.sdk or login_handle <= 0:
  323. return False
  324. params = self.SNAP_PARAMS()
  325. params.Channel = channel
  326. params.mode = 0 # SNAP_TYP_TIMING
  327. params.CmdSerial = cmd_serial
  328. params.PicTransType = 0
  329. params.Quality = 1
  330. params.PicFormat = 0 # BMP
  331. return self.sdk.CLIENT_SnapPicture(login_handle, byref(params))
  332. # PTZ控制命令常量 (从dhnetsdk.h提取)
  333. class PTZCommand:
  334. """PTZ控制命令常量"""
  335. # 基本控制
  336. UP = 0 # 上
  337. DOWN = 1 # 下
  338. LEFT = 2 # 左
  339. RIGHT = 3 # 右
  340. ZOOM_ADD = 4 # 变倍+
  341. ZOOM_DEC = 5 # 变倍-
  342. FOCUS_ADD = 6 # 聚焦+
  343. FOCUS_DEC = 7 # 聚焦-
  344. APERTURE_ADD = 8 # 光圈+
  345. APERTURE_DEC = 9 # 光圈-
  346. # 扩展控制
  347. EXACTGOTO = 23 # 三维精确定位 (param1:水平角0~3600, param2:垂直角-1800~1800, param3:变倍1~128)
  348. GOTOPRESET = 39 # 转到预置点
  349. MOVE_ABSOLUTELY = 41 # 绝对移动
  350. # 预置点控制
  351. POINT_SET = 16 # 设置预置点
  352. POINT_CLEAR = 17 # 清除预置点
  353. POINT_GO = 18 # 转到预置点
  354. class SDKError:
  355. """SDK错误码"""
  356. SUCCESS = 0
  357. NET_ERROR_PASSWORD = 1
  358. NET_ERROR_USER = 2
  359. NET_ERROR_TIMEOUT = 3
  360. NET_ERROR_RECONNECT = 4