Golang对接大华摄像头,调用大华C库下载视频

前言

嗯~,这次没图,全是干货。作为一个曾经的前端儿,现在为了生存转全干了。还是蛮喜欢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的发展还需要更多人的推广!!!

相关推荐
lekami_兰6 小时前
MySQL 长事务:藏在业务里的性能 “隐形杀手”
数据库·mysql·go·长事务
却尘9 小时前
一篇小白也能看懂的 Go 字符串拼接 & Builder & cap 全家桶
后端·go
ん贤10 小时前
一次批量删除引发的死锁,最终我选择不加锁
数据库·安全·go·死锁
mtngt111 天前
AI DDD重构实践
go
Grassto2 天前
12 go.sum 是如何保证依赖安全的?校验机制源码解析
安全·golang·go·哈希算法·go module
Grassto4 天前
11 Go Module 缓存机制详解
开发语言·缓存·golang·go·go module
程序设计实验室5 天前
2025年的最后一天,分享我使用go语言开发的电子书转换工具网站
go
我的golang之路果然有问题5 天前
使用 Hugo + GitHub Pages + PaperMod 主题 + Obsidian 搭建开发博客
golang·go·github·博客·个人开发·个人博客·hugo
啊汉7 天前
古文观芷App搜索方案深度解析:打造极致性能的古文搜索引擎
go·软件随想
asaotomo7 天前
一款 AI 驱动的新一代安全运维代理 —— DeepSentry(深哨)
运维·人工智能·安全·ai·go