EasyVQD/pkg/vqdcms/vqd.go
2026-01-28 14:00:33 +08:00

497 lines
13 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package vqdcms
/*
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
*/
import "C"
import (
"context"
"easyvqd/internal/core/host"
"easyvqd/pkg/decoder"
"fmt"
"log/slog"
"os"
"path/filepath"
"runtime/debug"
"strings"
"sync"
"sync/atomic"
"time"
"unsafe"
)
const MAX_STREAM_CHAN_NUM = 256
type VideoInfoVQD struct {
mu sync.RWMutex
VQDHandle uintptr
Params VQDPara
IsCreateSuccess bool
}
type VQDHandleInfo struct {
TaskID int
Plans string
TaskName string
ChannelID string
ChannelName string
PlanID int
PlanName string
TemplateID int
TemplateName string
}
type VQDHandle struct {
running uint32
ID int // 标识ID
info VQDHandleInfo
plansLock sync.RWMutex
playTicker *Scheduler
ErrorMsg string
data chan ChanData
dataLock sync.RWMutex
cb VQDResultCB
handle *VideoInfoVQD
name string // 算法名称
decoder *decoder.VideoDecoder
fileLock sync.RWMutex
hostCore *host.Core
Status VqdTaskStatus
}
type VQDResultCB func(AbnormalModel)
type ChanData struct {
data []byte
w int
h int
now time.Time
}
// IsCurTimeInRecordPlan 根据一周每天每小时的开关状态判断当前时间是否为开启状态
func IsCurTimeInRecordPlan(schedule string, now time.Time) bool {
if len(schedule) != 7*24 {
slog.Error("schedule length is not 7*24", "schedule", schedule)
return false
}
dayOfWeek := int(now.Weekday()+6) % 7 // 调整为0-6对应周一至周日
hourOfDay := now.Hour()
// 计算位置索引
index := (dayOfWeek * 24) + hourOfDay
// 检查索引位置的字符是否为'1'
if index >= 0 && index < len(schedule) && schedule[index] == '1' {
return true
}
return false
}
func NewVQDHandle(cb VQDResultCB, hostCore *host.Core, info VQDHandleInfo) *VQDHandle {
v := &VQDHandle{
running: 0,
decoder: &decoder.VideoDecoder{},
info: info,
ID: info.TaskID,
data: make(chan ChanData, MAX_STREAM_CHAN_NUM),
cb: cb,
handle: &VideoInfoVQD{},
playTicker: NewScheduler(),
hostCore: hostCore,
Status: TaskStatusStopped,
}
err := v.decoder.Create()
if err != nil {
slog.Error("decoder Create ", "taskId", info.ChannelID, "err", err)
}
v.StartPlay()
return v
}
func (v *VQDHandle) SetVQDConfig(params VQDPara, info VQDHandleInfo) error {
v.plansLock.Lock()
v.info = info
v.plansLock.Unlock()
return v.handle.Config(params, params.EnableFunc, 10)
}
func (v *VQDHandle) GetHandle() *VideoInfoVQD {
return v.handle
}
func (v *VQDHandle) StartPlay() {
v.Play()
v.playTicker.Start(25*time.Second, func() {
v.Play()
})
}
func (v *VQDHandle) Play() {
if IsCurTimeInRecordPlan(v.info.Plans, time.Now()) {
//slog.Info("vqd cms play", "taskId", v.TaskID, "chnId", v.ChnID)
_, errs := v.hostCore.Play(context.TODO(), &host.PlayInput{
ChannelID: v.info.ChannelID,
ActiveSecond: 40,
})
if errs != nil {
slog.Debug("vqd cms play", "taskId", v.info.TaskID, "chnId", v.info.ChannelID, "err", errs)
v.ErrorMsg = errs.Error()
v.Status = TaskStatusFailed
} else {
v.ErrorMsg = ""
v.Status = TaskStatusRunning
}
} else {
v.Status = TaskStatusStopped
}
}
func (v *VQDHandle) Create(params VQDPara, plan string) *VQDHandle {
v.plansLock.Lock()
v.info.Plans = plan
v.plansLock.Unlock()
if atomic.LoadUint32(&v.running) == 1 {
return v
}
err := v.handle.Create(params, params.EnableFunc, 10)
if err != nil {
slog.Error("vqd create", "taskId", v.info.TaskID, "chnId", v.info.ChannelID, "fail", err)
return v
}
atomic.StoreUint32(&v.running, 1)
go v.RunFrame()
return v
}
func (v *VQDHandle) Destroy() {
if v.data != nil {
close(v.data)
}
if v.decoder != nil {
err := v.decoder.Destroy()
if err != nil {
slog.Error("vqd decoder destroy", "taskId", v.info.TaskID, "chnId", v.info.ChannelID, "err", err)
}
}
v.decoder = nil
v.playTicker.Stop()
v.data = nil
if atomic.LoadUint32(&v.running) == 1 {
atomic.StoreUint32(&v.running, 0)
v.handle.Destroy()
}
}
func (v *VQDHandle) RunFrame() {
defer func() {
if e := recover(); e != nil {
print(fmt.Sprintf("RunFrame---%s\n", e))
print(fmt.Sprintf("%s\n", string(debug.Stack())))
}
}()
cvqdImgsDir := filepath.Join(CWD(), VQD_IMAGES_DIR, fmt.Sprintf("%d", v.info.TaskID))
cvqdImgsDir = filepath.ToSlash(cvqdImgsDir)
if err := os.MkdirAll(cvqdImgsDir, os.ModePerm); err != nil {
slog.Error("vqd create img dir", "taskId", v.info.TaskID, "chnId", v.info.ChannelID, "err", err)
}
hyper := 0
index := 0
cdata := ChanData{}
for {
select {
case data, ok := <-v.data:
if !ok {
return
}
if len(v.data) >= (MAX_STREAM_CHAN_NUM - 6) {
slog.Error("vqd channel num", "taskId", v.info.TaskID, "chnId", v.info.ChannelID, ">= ", MAX_STREAM_CHAN_NUM)
hyper = MAX_STREAM_CHAN_NUM / 10
}
if hyper > 0 {
hyper--
break
}
cdata = data
now := time.Now().UnixMilli()
fpath := filepath.Join(cvqdImgsDir, fmt.Sprintf("%s_%d_%d_%d.jpg", v.info.ChannelID, v.info.TemplateID, v.info.PlanID, now))
fpath = filepath.ToSlash(fpath)
result := VQDResult{}
ret := v.handle.Frame(cdata.data, cdata.w, cdata.h, index, fpath, &result)
if ret == 0 {
index = 0
if value, b := v.parseVQD(result); b {
value.FilePath = strings.TrimPrefix(filepath.ToSlash(fpath), filepath.ToSlash(CWD()))
value.TaskName = v.info.TaskName
value.ID = v.info.TaskID
value.ChannelID = v.info.ChannelID
value.ChannelName = v.info.ChannelName
value.PlanID = v.info.PlanID
value.PlanName = v.info.PlanName
value.TemplateID = v.info.TemplateID
value.TemplateName = v.info.TemplateName
if v.cb != nil {
v.cb(value)
} else {
// 保存数据库
//CreateAbnormalModel(&value)
}
}
}
//C.free(fp)
//C.free(yuvBuf)
index++
}
}
}
// VQD_ENABLE_COLOR = "vqd_color"
// VQD_ENABLE_LGTDARK = "vqd_lgt_dark"
// VQD_ENABLE_CLARITY = "vqd_clarity"
// VQD_ENABLE_NOISE = "vqd_noise"
// VQD_ENABLE_CONTRAST = "vqd_contrast"
// VQD_ENABLE_OCCLUSION = "vqd_occlusion"
// VQD_ENABLE_BLUE = "vqd_blue"
// VQD_ENABLE_SHARK = "vqd_shark"
// VQD_ENABLE_FREEZE = "vqd_freeze"
// VQD_ENABLE_MOSAIC = "vqd_mosaic"
// VQD_ENABLE_FLOWER = "vqd_flower"
func (v *VQDHandle) parseVQD(result VQDResult) (AbnormalModel, bool) {
isabnormal := false
abnormals := AbnormalModel{
IsDeep: v.handle.Params.UseDeepLearning,
Abnormals: make([]Abnormal, 0),
}
if (result.AbnormalType & NXU_VQD_ABN_COLORDEV) != 0 {
isabnormal = true
abnormals.Abnormals = append(abnormals.Abnormals, Abnormal{
Value: result.ColorDev,
Name: ALNORMAL_NAMES[NXU_VQD_ABN_COLORDEV],
Mode: "vqd_color",
})
}
abnormals.DefaultValues = append(abnormals.DefaultValues, DefaultValue{
Thr1: v.handle.Params.ColorPara.ColorThr,
Name1: "偏色阈值",
Ratio: v.handle.Params.ColorPara.ColorAbnNumRatio,
})
if (result.AbnormalType & NXU_VQD_ABN_LIGHT) != 0 {
isabnormal = true
abnormals.Abnormals = append(abnormals.Abnormals, Abnormal{
Value: result.LgtDark,
Name: ALNORMAL_NAMES[NXU_VQD_ABN_LIGHT],
Mode: "vqd_lgt_light",
})
}
if (result.AbnormalType & NXU_VQD_ABN_DARK) != 0 {
isabnormal = true
abnormals.Abnormals = append(abnormals.Abnormals, Abnormal{
Value: result.LgtDark,
Name: ALNORMAL_NAMES[NXU_VQD_ABN_DARK],
Mode: "vqd_lgt_dark",
})
}
abnormals.DefaultValues = append(abnormals.DefaultValues, DefaultValue{
Thr1: v.handle.Params.LgtDarkPara.LightThr,
Name1: "过亮阈值",
Thr2: v.handle.Params.LgtDarkPara.DarkThr,
Name2: "过暗阈值",
Ratio: v.handle.Params.LgtDarkPara.LgtDarkAbnNumRatio,
})
if (result.AbnormalType & NXU_VQD_ABN_CLARITY) != 0 {
isabnormal = true
abnormals.Abnormals = append(abnormals.Abnormals, Abnormal{
Value: result.Clarity,
Name: ALNORMAL_NAMES[NXU_VQD_ABN_CLARITY],
Mode: "vqd_clarity",
})
}
abnormals.DefaultValues = append(abnormals.DefaultValues, DefaultValue{
Thr1: v.handle.Params.ClarityPara.ClarityThr,
Name1: "清晰度阈值",
Ratio: v.handle.Params.ClarityPara.ClarityAbnNumRatio,
})
if (result.AbnormalType & NXU_VQD_ABN_NOISE) != 0 {
isabnormal = true
abnormals.Abnormals = append(abnormals.Abnormals, Abnormal{
Value: result.Noise,
Name: ALNORMAL_NAMES[NXU_VQD_ABN_NOISE],
Mode: "vqd_noise",
})
}
abnormals.DefaultValues = append(abnormals.DefaultValues, DefaultValue{
Thr1: v.handle.Params.NoisePara.NoiseThr,
Name1: "噪声阈值",
Ratio: v.handle.Params.NoisePara.NoiseAbnNumRatio,
})
if (result.AbnormalType & NXU_VQD_ABN_CONTRAST) != 0 {
isabnormal = true
abnormals.Abnormals = append(abnormals.Abnormals, Abnormal{
Value: result.Contrast,
Name: ALNORMAL_NAMES[NXU_VQD_ABN_CONTRAST],
Mode: "vqd_contrast",
})
}
abnormals.DefaultValues = append(abnormals.DefaultValues, DefaultValue{
Thr1: v.handle.Params.ContrastPara.CtraLowThr,
Name1: "低对比度阈值",
Thr2: v.handle.Params.ContrastPara.CtraHighThr,
Name2: "高对比度阈值",
Ratio: v.handle.Params.ContrastPara.CtraAbnNumRatio,
})
if (result.AbnormalType & NXU_VQD_ABN_OCCLUSION) != 0 {
isabnormal = true
abnormals.Abnormals = append(abnormals.Abnormals, Abnormal{
Value: result.Occlusion,
Name: ALNORMAL_NAMES[NXU_VQD_ABN_OCCLUSION],
Mode: "vqd_occlusion",
})
}
abnormals.DefaultValues = append(abnormals.DefaultValues, DefaultValue{
Thr1: v.handle.Params.OcclusionPara.OcclusionThr,
Name1: "遮挡阈值",
Ratio: v.handle.Params.OcclusionPara.OcclusionAbnNumRatio,
})
if (result.AbnormalType & NXU_VQD_ABN_BLUE) != 0 {
isabnormal = true
abnormals.Abnormals = append(abnormals.Abnormals, Abnormal{
Value: result.Blue,
Name: ALNORMAL_NAMES[NXU_VQD_ABN_BLUE],
Mode: "vqd_blue",
})
}
abnormals.DefaultValues = append(abnormals.DefaultValues, DefaultValue{
Thr1: v.handle.Params.BluePara.BlueThr,
Name1: "蓝屏阈值",
Ratio: v.handle.Params.BluePara.BlueAbnNumRatio,
})
if (result.AbnormalType & NXU_VQD_ABN_SHARK) != 0 {
isabnormal = true
abnormals.Abnormals = append(abnormals.Abnormals, Abnormal{
Value: result.Shark,
Name: ALNORMAL_NAMES[NXU_VQD_ABN_SHARK],
Mode: "vqd_shark",
})
}
abnormals.DefaultValues = append(abnormals.DefaultValues, DefaultValue{
Thr1: v.handle.Params.SharkPara.SharkThr,
Name1: "抖动阈值",
Ratio: v.handle.Params.SharkPara.SharkAbnNumRatio,
})
if (result.AbnormalType & NXU_VQD_ABN_FREEZE) != 0 {
isabnormal = true
abnormals.Abnormals = append(abnormals.Abnormals, Abnormal{
Value: result.Freeze,
Name: ALNORMAL_NAMES[NXU_VQD_ABN_FREEZE],
Mode: "vqd_freeze",
})
}
abnormals.DefaultValues = append(abnormals.DefaultValues, DefaultValue{
Thr1: v.handle.Params.FreezePara.FreezeThr,
Name1: "冻结阈值",
Ratio: v.handle.Params.FreezePara.FreezeAbnNumRatio,
})
if (result.AbnormalType & NXU_VQD_ABN_MOSAIC) != 0 {
isabnormal = true
abnormals.Abnormals = append(abnormals.Abnormals, Abnormal{
Value: result.Mosaic,
Name: ALNORMAL_NAMES[NXU_VQD_ABN_MOSAIC],
Mode: "vqd_mosaic",
})
}
abnormals.DefaultValues = append(abnormals.DefaultValues, DefaultValue{
Thr1: v.handle.Params.MosaicPara.MosaicThr,
Name1: "马赛克阈值",
Ratio: v.handle.Params.MosaicPara.MosaicAbnNumRatio,
})
if (result.AbnormalType & NXU_VQD_ABN_FLOWER) != 0 {
isabnormal = true
abnormals.Abnormals = append(abnormals.Abnormals, Abnormal{
Value: result.Flower,
Name: ALNORMAL_NAMES[NXU_VQD_ABN_FLOWER],
Mode: "vqd_flower",
})
}
abnormals.DefaultValues = append(abnormals.DefaultValues, DefaultValue{
Thr1: v.handle.Params.FlowerPara.FlowerThr,
Name1: "花屏阈值",
Ratio: v.handle.Params.FlowerPara.FlowerAbnNumRatio,
})
return abnormals, isabnormal
}
func (v *VQDHandle) SendData(buf []byte, _codec int) {
if !IsCurTimeInRecordPlan(v.info.Plans, time.Now()) {
return
}
w, h, data, err := v.decoder.PushDataEx(buf, _codec)
if err != nil {
slog.Error("I帧转YUV失败: ", "TaskID", v.info.TaskID, "err", err)
return
}
if len(data) > 0 && v.data != nil {
dst := make([]byte, len(data))
copy(dst, data)
now := time.Now()
d := ChanData{
data: dst,
w: w,
h: h,
now: now,
}
v.data <- d
}
}
// IFrameData 封装I帧数据和指针信息
type IFrameData struct {
Data []byte // I帧原始字节数据
Pointer unsafe.Pointer // 指向数据的原始指针
Length int // 数据长度(字节数)
IsValid bool // 指针是否有效
}
// GetIFramePointer 将字节切片转换为原始指针
// 注意unsafe包的使用会绕过Go的内存安全检查需谨慎
func GetIFramePointer(data []byte) *IFrameData {
if len(data) == 0 {
return &IFrameData{
IsValid: false,
Length: 0,
}
}
// 方式1直接通过unsafe获取切片底层数组的指针推荐高效
// 切片的底层结构是:指向数组的指针 + 长度 + 容量
ptr := unsafe.Pointer(&data[0])
// 方式2通过reflect获取指针更直观展示切片结构可选
// sliceHeader := (*reflect.SliceHeader)(unsafe.Pointer(&data))
// ptr := unsafe.Pointer(sliceHeader.Data)
return &IFrameData{
Data: data,
Pointer: ptr,
Length: len(data),
IsValid: true,
}
}
func CWD() string {
geTwd, err := os.Getwd()
if err != nil {
return ""
}
return geTwd
}