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的发展还需要更多人的推广!!!

相关推荐
郝同学的测开笔记1 天前
云原生探索系列(十二):Go 语言接口详解
后端·云原生·go
一点一木2 天前
WebAssembly:Go 如何优化前端性能
前端·go·webassembly
千羽的编程时光3 天前
【CloudWeGo】字节跳动 Golang 微服务框架 Hertz 集成 Gorm-Gen 实战
go
27669582924 天前
阿里1688 阿里滑块 231滑块 x5sec分析
java·python·go·验证码·1688·阿里滑块·231滑块
Moment5 天前
在 NodeJs 中如何通过子进程与 Golang 进行 IPC 通信 🙄🙄🙄
前端·后端·go
唐僧洗头爱飘柔95275 天前
(Go基础)变量与常量?字面量与变量的较量!
开发语言·后端·golang·go·go语言初上手
黑心萝卜三条杠6 天前
【Go语言】深入理解Go语言:并发、内存管理和垃圾回收
google·程序员·go
不喝水的鱼儿6 天前
【LuatOS】基于WebSocket的同步请求框架
网络·websocket·网络协议·go·luatos·lua5.4
微刻时光6 天前
程序员开发速查表
java·开发语言·python·docker·go·php·编程语言
lidenger7 天前
服务认证-来者何人
后端·go