前言
嗯~,这次没图,全是干货。作为一个曾经的前端儿,现在为了生存转全干了。还是蛮喜欢golang的,希望社区能早日壮大,少点水文。第一个项目就是独立开发对接三方,折腾了一周。跟着文章走,做不出来你打我!
你需要准备什么?
1.进入大华官网找到对应的sdk包,根据你们的需要自己下载。
2.准备好自己项目,并且get windows库
shell
$ go get golang.org/x/sys/windows
3.对C语言数据类型有一点点了解 部分类型对应如下:
C语言数据类型 | golang数据类型 |
---|---|
LONG | int32 |
LLONG | int64 |
UINT | uint32 |
DWORD | uint32 |
LDWORD | uint64 |
LPVOID | uintptr |
LPDWORD | *uint32 |
BOOL | bool |
BYTE | byte |
BYTE * | []byte |
char * | *byte |
void* | uintptr |
char | byte |
初始化大华SDK
1.加载dll文件 先准备好自己的项目,这是我的项目截图
将你下载好的大华sdk文件解压后,将里面的.dll文件和.lib文件都放入bin目录下 然后在apps目录下新建init.go
go
package apps
var (
dhnetsdk *windows.LazyDLL // 定义大华sdk,用来加载dll
)
func init() {
dhnetsdk = windows.NewLazyDLL("bin/dhnetsdk") // 具体根据你自己的文件路径
}
// 用来加载C库SDK中的函数
func DHNETSDK(name string, a ...uintptr) uintptr {
r1, r2, lastErr := dhnetsdk.NewProc(name).Call(a...)
if lastErr.Error() != "The operation completed successfully." {
log.Println("DHNETSDK 调用出错", name, r1, r2, lastErr)
}
return r1
}
2.初始化 在apps文件夹下新建sdk文件夹,并在其中新建init.go
go
package sdk
// 初始化参数
type NETSDK_INIT_PARAM struct {
nThreadNum int32 // 指定NetSDK常规网络处理线程数,当值为0时,使用内部默认值
bReserved [1024]byte // 保留字节
}
// 网络连接断开回调函数原形
func cbDisConnect(lLoginID int64, pchDVRIP *byte, nDVRPort int64, dwUser int64) uintptr {
var pchDVRIP_ string
if pchDVRIP != nil {
pchDVRIP_ = apps.I_b2s(pchDVRIP)
}
fmt.Println("连接断开", lLoginID, pchDVRIP_, nDVRPort, dwUser)
_ = os.Stdout.Sync()
return 0
}
// SDKInit SDK初始化
func SDKInit(dwUser interface{}) (interface{}, error) {
var lpInitParam NETSDK_INIT_PARAM
lpInitParam.nThreadNum = 0
result := apps.DHNETSDK("CLIENT_Init", windows.NewCallback(cbDisConnect), uintptr(dwUser.(int64)))
if result == 1 {
fmt.Printf("DHNETSDK 初始化成功\n")
return result, nil
} else {
fmt.Printf("DHNETSDK 初始化失败, errorCode: %v\n", result)
return result, fmt.Errorf("DHNETSDK 初始化失败, errorCode: %v\n", result)
}
}
// SDK退出清理
func ExitSDKClean() {
apps.DHNETSDK("CLIENT_Cleanup")
}
相信有人看到一定会问(质疑)为什么我的函数入参和返回值都有interface{},这是因为我要开发的是一个cli需要将所有的函数包裹在中间件里,所以为了满足中间件签名才这样。在初始化中其实还有Ex初始化方式,我就没有写了,因为我这不用。具体的需求可以看官方文档。
登录
在sdk初始化完成后,才能进行登录。登录有两种方式,第一种是官方推荐的高安全级别,另一种是Ex登录方式,我这里就用Ex登录了,主要是懒。
go
type LoginWithEx2Req struct {
PchDVRIP string
PchUserName string
PchPassword string
WDVRPort int32
}
// LoginWithEx2 登入Ex2
// LoginWithEx2(pchDVRIP, pchUserName, pchPassword string, wDVRPort int32) (int64, error)
func LoginWithEx2(req interface{}) (interface{}, error) {
var pchDVRIP, pchUserName, pchPassword string
var wDVRPort int32
switch req.(type) {
case LoginWithEx2Req:
req := req.(LoginWithEx2Req)
pchDVRIP = req.PchDVRIP
pchUserName = req.PchUserName
pchPassword = req.PchPassword
wDVRPort = req.WDVRPort
default:
return nil, fmt.Errorf("LoginWithEx2 参数类型错误")
}
var lpDeviceInfo NET_DEVICEINFO_Ex
var err = 0
lLoginID := apps.DHNETSDK("CLIENT_LoginEx2", uintptr(unsafe.Pointer(apps.I_s2b(pchDVRIP))), uintptr(wDVRPort),
uintptr(unsafe.Pointer(apps.I_s2b(pchUserName))), uintptr(unsafe.Pointer(apps.I_s2b(pchPassword))), uintptr(EM_LOGIN_SPEC_CAP_TCP),
uintptr(unsafe.Pointer(nil)), uintptr(unsafe.Pointer(&lpDeviceInfo)), uintptr(unsafe.Pointer(&err)))
if lLoginID == 0 {
fmt.Println("DHNETSDK 登录失败", pchDVRIP, err)
_ = os.Stdout.Sync()
return int64(lLoginID), fmt.Errorf("DHNETSDK 登录失败, %v, %v", pchDVRIP, err)
}
fmt.Println("DHNETSDK 登录成功", pchDVRIP, lLoginID)
fmt.Println("序列号:", string(lpDeviceInfo.sSerialNumber[:]), "通道数:", lpDeviceInfo.nChanNum)
_ = os.Stdout.Sync()
return int64(lLoginID), nil
}
// LogOut 注销登录
func LogOut(lLoginID interface{}) (interface{}, error) {
if lLoginID == nil || lLoginID == 0 {
return nil, fmt.Errorf("LogOut 登录ID不能为空")
}
result := apps.DHNETSDK("CLIENT_Logout", uintptr(lLoginID.(int64)))
if result == 0 {
fmt.Println("DHNETSDK 注销登录失败", lLoginID)
_ = os.Stdout.Sync()
return nil, fmt.Errorf("DHNETSDK 注销登录失败, %v", lLoginID)
} else {
fmt.Println("DHNETSDK 注销登录成功", lLoginID)
_ = os.Stdout.Sync()
return nil, nil
}
}
下载视频
在我们成功登录后可以拿到返回的loginId,后序我们都需要用这个loginId,所以可以定义一个全局变量。 需要注意的是下载视频的函数里还有回调函数,然后回调函数里还有回调函数。但是他回调的回调虽然作为参数要求你传递进去,但是如果不是回放的函数,反而不会去触发回调的回调。当然也可能是我自己的问题,如果有人发现了问题欢迎随时指教。我必定虚心学习。
go
type RECORD_FILE_TYPE int
const (
ALL_FILE_TYPE RECORD_FILE_TYPE = iota // 所有录像文件
OUT_ALARM_TYPE // 外部报警
DYNAMIC_ALAMR_TYPE //动态检测报警
ALL_ALAMR_TYPE // 所有报警
CARD_SEARCH_TYPE //卡号查询
COMBINATION_CONDITION_SEARCH_TYPE // 组合条件查询
FILE_PATH_AND_DEVIATION_TYPE // 录像位置与偏移量长度
_ // 跳过
BY_CARD_TO_SEARCH_IMAGE_TYPE // 按卡号查询图片(目前仅HB-U和NVS特殊型号的设备支持)
SEARCH_IMAGE_TYPE // 查询图片(目前仅HB-U和NVS特殊型号的设备支持)
BY_FIELD_TYPE // 按字段查询
_ = iota + 3 //跳过
RETURN_NET_FRAME_TYPE // 返回网络数据结构
SEARCH_ALL_TRANSPARENT_STRING_DATA_VIDEO_FILES_TYPE //查询所有透明串数据录像文件
)
type NRecordFileType byte
const (
REGULAR_VIDEO_RECORDING_TYPE NRecordFileType = iota // 普通录象
ALARM_RECORDING_TYPE // 报警录象
MOVEMENT_DETECTION // 移动检测
CARD_NUMBER_RECORDING_TYPE // 卡号录象
IMAGE_TYPE // 图片
)
type bImportantRecID byte
const (
PUBLIC_VIDEO_TYPE bImportantRecID = iota // 普通录象
IMPORTANT_VIDEO_TYPE // 重要录象
)
// 录像文件信息,适用于下载视频
type NET_RECORDFILE_INFO struct {
Ch uint32 // 通道号
Filename [128]byte // 文件名
Framenum uint32 // 文件总帧数
Size uint32 // 文件长度
Starttime NET_TIME // 开始时间
Endtime NET_TIME // 结束时间
Driveno uint32 // 磁盘号(区分网络录像和本地录像的类型,0-127表示本地录像,其中64表示光盘1,128表示网络录像)
Startcluster uint32 // 起始簇号
NRecordFileType NRecordFileType
BImportantRecID bImportantRecID
BHint byte // 文件定位索引
BRecType byte // 0-主码流录像 1-辅码1流录像 2-辅码流2 3-辅码流3录像
}
type DownloadByTimeReq struct {
LoginId int64
ChannelId int
RecordFileType RECORD_FILE_TYPE
StartTime NET_TIME
EndTime NET_TIME
SaveFilePath string
DwUserData uint64
DwDataUser uint64
Done *sync.WaitGroup // 用于通知下载完成
}
var (
downloadSuccess = false // 是否下载完成
cbTimeLoadCount int = 0 // 用于记录回调函数被调用的次数
fdLoadDataCount int = 0 // 用于记录回调函数被调用的次数
)
func DownloadVideoByTime(req interface{}) (interface{}, error) {
//fmt.Println("执行DownloadVideoByTime函数")
//将 req 转换为 DownloadByTimeReq 类型
downloadByTimeReq, ok := req.(DownloadByTimeReq)
if !ok {
return nil, fmt.Errorf("类型转换失败")
}
loginId := downloadByTimeReq.LoginId
channelId := downloadByTimeReq.ChannelId
recordFileType := downloadByTimeReq.RecordFileType
startTime := downloadByTimeReq.StartTime
endTime := downloadByTimeReq.EndTime
saveFilePath := downloadByTimeReq.SaveFilePath
dwUserData := downloadByTimeReq.DwUserData
dwDataUser := downloadByTimeReq.DwDataUser
wg := downloadByTimeReq.Done
//启动一个 goroutine 执行下载操作
go func() {
// 获取 startTime 的内存地址,然后转换为 uintptr
startTimePtr := uintptr(unsafe.Pointer(&startTime))
// 获取 startTime 的内存地址,然后转换为 uintptr
endTimePtr := uintptr(unsafe.Pointer(&endTime))
// 获取 cbTimeDownLoadPos 的内存地址,然后转换为 uintptr
cbTimeDownLoadPosPtr := windows.NewCallback(CbTimeDownLoadPos)
// 获取 fDownLoadDataCallBack 的内存地址,然后转换为 uintptr
fDownLoadDataCallBackPtr := windows.NewCallback(FDownLoadDataCallBack)
//fmt.Printf("saveFilePath: %s\n", saveFilePath)
_ = os.Stdout.Sync()
result := apps.DHNETSDK("CLIENT_DownloadByTimeEx", uintptr(loginId), uintptr(channelId), uintptr(recordFileType), startTimePtr, endTimePtr,
uintptr(unsafe.Pointer(apps.I_s2b(saveFilePath))), cbTimeDownLoadPosPtr, uintptr(dwUserData),
fDownLoadDataCallBackPtr, uintptr(dwDataUser), uintptr(unsafe.Pointer(wg)))
if result == 0 {
fmt.Printf("视频开始下载失败: %v\n", result)
defer wg.Done()
return
} else {
fmt.Printf("视频开始下载成功, id: %v\n", result)
for {
if downloadSuccess {
//fmt.Printf("cbTimeLoadCount: %v, fdLoadDataCount: %v\n", cbTimeLoadCount, fdLoadDataCount)
fmt.Println("视频下载完成")
wg.Done()
break
}
}
}
}()
wg.Wait()
return nil, nil
}
type TimeCallBack func(uintptr, uintptr, uintptr, uintptr, uintptr) uintptr
func CbTimeDownLoadPos(loginId uintptr, lpRecordFile uintptr, hWnd uintptr, callback uintptr, dwUserData uintptr) uintptr {
//fmt.Println("CbTimeDownLoadPos called")
cbTimeLoadCount++
lpRecordFilePtr := uintptr(unsafe.Pointer(&lpRecordFile))
CallBackFuncFromPosPtr := windows.NewCallback(CbDownLoadPos)
apps.DHNETSDK("CLIENT_PlayBackByRecordFile", loginId, lpRecordFilePtr, hWnd, CallBackFuncFromPosPtr, dwUserData)
if cbTimeLoadCount != fdLoadDataCount {
//fmt.Printf("cbTimeLoadCount: %v, fdLoadDataCount: %v\n", cbTimeLoadCount, fdLoadDataCount)
downloadSuccess = true
}
return 0
}
// CallBackFuncFromPos 是TimeCallBack回调函数的回调函数
type CallBackFuncFromPos func(int64, uint32, uint32, uint64)
func CbDownLoadPos(lPlayHandle int64, dwTotalSize, dwDownLoadSize uint32, dwUser uint64) uintptr {
fmt.Println("CbDownLoadPos called")
fmt.Printf("lPlayHandle: %v, dwTotalSize: %v, dwDownLoadSize: %v, dwUser: %v\n", lPlayHandle, dwTotalSize, dwDownLoadSize, dwUser)
return 0
}
//type DataCallBack func(int64, uint32, *byte, uint32, uint64)
//func FDownLoadDataCallBack(lPlayHandle int64, dwDataType uint32, pBuffer *byte, dwBufSize uint32, dwUser uint64) {
// fmt.Printf("lplayHandle: %v, dwDateType: %v, dwBufSize: %v", lPlayHandle, dwDataType, dwBufSize)
//}
type DataCallBack func(uintptr, uintptr, uintptr, uintptr, uintptr) uintptr
var totalSize int
func FDownLoadDataCallBack(lPlayHandle uintptr, dwDataType uintptr, pBuffer uintptr, dwBufSize uintptr, dwUser uintptr, wg uintptr) uintptr {
//fmt.Println("FDownLoadDataCallBack called")
fdLoadDataCount++
// 将 uintptr 类型的值转换为指针
pBufferPtr := (*byte)(unsafe.Pointer(pBuffer))
// 如果 pBufferPtr 不是 nil,那么你可以通过 pBufferPtr 来操作 pBuffer 指向的数据
// 例如,你可以打印出 pBufferPtr 指向的第一个字节的值
if pBufferPtr != nil && dwBufSize > 0 {
//fmt.Printf("First byte of pBuffer: %v\n", *pBufferPtr)
//fmt.Println(apps.I_b2s(pBufferPtr))
}
//totalSize += int(dwBufSize)
fmt.Printf("dwBufSize: %v\n", dwBufSize)
// 如果int64(dwBufSize) == 0, 那么表示数据已经下载完成
//if int64(dwBufSize) == 0 && totalSize > 0 {
// fmt.Println("数据下载完成")
// wg := *(*sync.WaitGroup)(unsafe.Pointer(wg)) // 将 uintptr 转换为 *sync.WaitGroup
// defer wg.Done()
//}
_ = os.Stdout.Sync()
return 0
}
需要注意的是,我们下载视频是一个goroutine,所以需要等待所有的视频下载完成才能结束进程。我至今不解的是没找到能返回给我下载文件总大小的参数。所以如果要计算下载进度可能还需要自己计算码流。
附言
因为是第一次并且是独立开发的项目,难免有不好的地方,欢迎各位大佬提出修改建议。然后也希望未来大家能多开源一些实用好用的东西,毕竟go的发展还需要更多人的推广!!!