285 lines
7.7 KiB
Go
285 lines
7.7 KiB
Go
package vqdtask
|
||
|
||
import (
|
||
"bufio"
|
||
"easyvqd/internal/conf"
|
||
"easyvqd/internal/core/host"
|
||
"easyvqd/internal/core/vqd"
|
||
"easyvqd/pkg/vqdcms"
|
||
"fmt"
|
||
"log/slog"
|
||
"os"
|
||
"os/exec"
|
||
"path/filepath"
|
||
"strings"
|
||
"time"
|
||
)
|
||
|
||
type Core struct {
|
||
HostCore *host.Core
|
||
VqdTaskCore *vqd.Core
|
||
Cfg *conf.Bootstrap
|
||
}
|
||
|
||
var (
|
||
VqdTaskMap = vqdcms.VqdTaskMap{M: make(map[string]*vqdcms.VQDHandle)}
|
||
)
|
||
|
||
func NewCore(HostCore *host.Core, VqdTaskCore *vqd.Core, Cfg *conf.Bootstrap) *Core {
|
||
core := &Core{
|
||
HostCore: HostCore,
|
||
VqdTaskCore: VqdTaskCore,
|
||
Cfg: Cfg,
|
||
}
|
||
core.HostCore.CbIFrame = func(s string, data []byte, codes int) {
|
||
//fmt.Println("res", s, codes, len(data))
|
||
v, ok := VqdTaskMap.LoadTaskMap(s)
|
||
if ok {
|
||
v.SendData(data, codes)
|
||
}
|
||
}
|
||
|
||
time.AfterFunc(time.Duration(5)*time.Second, func() {
|
||
//in := &vqd.AddVqdAlarmInput{
|
||
// AlarmName: "遮挡告警",
|
||
// AlarmValue: "",
|
||
// ChannelID: "",
|
||
// ChannelName: "",
|
||
// TaskTemplateID: 0,
|
||
// TaskTemplateName: "",
|
||
// TaskID: 0,
|
||
// TaskName: "",
|
||
// FilePath: "",
|
||
//}
|
||
//for i := 0; i < 2; i++ {
|
||
// core.VqdTaskCore.AddVqdAlarm(context.TODO(), in)
|
||
//}
|
||
|
||
// 启用诊断分析
|
||
core.InitVqdTask()
|
||
core.AddTaskVqd(1, "PVWPQBPIv32UI_01")
|
||
})
|
||
// 启用定时清理任务
|
||
go core.scheduleCleanTask()
|
||
// 测试
|
||
//go core.OpenStartVqd()
|
||
// 启用任务管理器
|
||
return core
|
||
}
|
||
|
||
func (c Core) InitVqdTask() {
|
||
err := vqdcms.VQDInit()
|
||
if err != nil {
|
||
slog.Error("vqd cms open", "err", err.Error())
|
||
return
|
||
}
|
||
|
||
return
|
||
}
|
||
func (c Core) UnVqdTask() {
|
||
vqdcms.VQDUnInit()
|
||
return
|
||
}
|
||
func (c Core) AddTaskVqd(taskId int, chnId string) {
|
||
cb := func(res vqdcms.AbnormalModel) {
|
||
fmt.Println("res", res)
|
||
}
|
||
para := vqdcms.NewVQDPara()
|
||
v := vqdcms.NewVQDHandle(cb, taskId, chnId).Create(para)
|
||
VqdTaskMap.StoreChildMap(fmt.Sprintf("%s", chnId), v)
|
||
}
|
||
|
||
//func (c Core) OpenStartVqd() {
|
||
//
|
||
// err := vqdcms.VQDInit()
|
||
// if err != nil {
|
||
// fmt.Println("程序异常", err.Error())
|
||
// return
|
||
// }
|
||
// dir, _ := os.Getwd()
|
||
// rootPath := filepath.Join(dir, "gbs_buf264") // 你的H.264裸流文件路径
|
||
//
|
||
// v := vqdcms.NewVQDHandle(nil, 1)
|
||
//
|
||
// para := vqdcms.NewVQDPara()
|
||
// v.SetVQDConfig(para)
|
||
// v.Create(para)
|
||
// entries, err := os.ReadDir(rootPath)
|
||
// if err != nil {
|
||
// fmt.Printf("读取目录失败: %v\n", err)
|
||
// return
|
||
// }
|
||
//
|
||
// fmt.Printf("目录 %s 下的内容:\n", dir)
|
||
// for _, entry := range entries {
|
||
// if entry.IsDir() {
|
||
// fmt.Printf("[目录] %s\n", entry.Name())
|
||
// } else {
|
||
// h264Paths := filepath.Join(rootPath, entry.Name()) // 你的H.264裸流文件路径
|
||
// fmt.Println(h264Paths)
|
||
// data, err := os.ReadFile(h264Paths)
|
||
// if err == nil {
|
||
// datap := GetIFramePointer(data)
|
||
// width, height, buf, err := v.de.PushDataEx(uintptr(datap.Pointer), datap.Length, VIDEO_CODEC_H264)
|
||
// if err == nil {
|
||
// v.SendData(buf, width, height)
|
||
// slog.Info("I帧转YUV成功: ", "h264Paths", h264Paths)
|
||
// } else {
|
||
// //slog.Error("I帧转YUV失败: ", "h264Paths", h264Paths )
|
||
// }
|
||
// }
|
||
// }
|
||
// }
|
||
//
|
||
// return
|
||
//}
|
||
//
|
||
//// H264IFrameData 封装I帧数据和指针信息
|
||
//type H264IFrameData struct {
|
||
// Data []byte // I帧原始字节数据
|
||
// Pointer unsafe.Pointer // 指向数据的原始指针
|
||
// Length int // 数据长度(字节数)
|
||
// IsValid bool // 指针是否有效
|
||
//}
|
||
//
|
||
//// GetIFramePointer 将字节切片转换为原始指针
|
||
//// 注意:unsafe包的使用会绕过Go的内存安全检查,需谨慎
|
||
//func GetIFramePointer(data []byte) *H264IFrameData {
|
||
// if len(data) == 0 {
|
||
// return &H264IFrameData{
|
||
// 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 &H264IFrameData{
|
||
// Data: data,
|
||
// Pointer: ptr,
|
||
// Length: len(data),
|
||
// IsValid: true,
|
||
// }
|
||
//}
|
||
|
||
// 测试i帧数据是否可以转为图片文件
|
||
func CheckIFramesToJpg() {
|
||
dir, _ := os.Getwd()
|
||
|
||
h264Path := filepath.Join(dir, "check_tip.h264") // 你的H.264裸流文件路径
|
||
//1. 检查FFmpeg
|
||
ok, err := CheckFFmpeg()
|
||
if !ok {
|
||
fmt.Println("错误:", err)
|
||
os.Exit(1)
|
||
}
|
||
|
||
// 2. 配置参数(根据你的H.264文件调整)
|
||
|
||
imageFormat := "jpg" // 输出图片格式
|
||
videoWidth := 0 // 分辨率自动探测(如需指定则改为实际值,如1920)
|
||
videoHeight := 0 // 分辨率自动探测(如需指定则改为实际值,如1080)
|
||
|
||
// 3. 提取H.264关键帧并转图片
|
||
if err := ExtractH264KeyFrames(h264Path, dir, imageFormat, videoWidth, videoHeight); err != nil {
|
||
fmt.Println("提取H.264关键帧失败:", err)
|
||
os.Exit(1)
|
||
}
|
||
}
|
||
|
||
// CheckFFmpeg 检查系统是否安装了FFmpeg
|
||
func CheckFFmpeg() (bool, error) {
|
||
cmd := exec.Command("ffmpeg", "-version")
|
||
err := cmd.Run()
|
||
if err != nil {
|
||
return false, fmt.Errorf("FFmpeg 未安装或未添加到系统PATH: %v", err)
|
||
}
|
||
return true, nil
|
||
}
|
||
|
||
// ExtractH264KeyFrames 将H.264裸流的关键帧转为图片
|
||
// h264Path: H.264裸流文件路径(.264/.h264)
|
||
// outputDir: 图片输出目录
|
||
// format: 输出图片格式 (jpg/png)
|
||
// width/height: 视频分辨率(若不指定,FFmpeg会自动探测)
|
||
func ExtractH264KeyFrames(h264Path, outputDir, format string, width, height int) error {
|
||
// 验证输入文件
|
||
if _, err := os.Stat(h264Path); os.IsNotExist(err) {
|
||
return fmt.Errorf("H.264文件不存在: %s", h264Path)
|
||
}
|
||
|
||
// 创建输出目录
|
||
if err := os.MkdirAll(outputDir, 0755); err != nil {
|
||
return fmt.Errorf("创建输出目录失败: %v", err)
|
||
}
|
||
|
||
// 构造FFmpeg命令(适配H.264裸流)
|
||
outputPattern := filepath.Join(outputDir, "h264_keyframe_%04d."+format)
|
||
args := []string{
|
||
"-f", "h264", // 明确输入格式为H.264裸流
|
||
"-i", h264Path, // 输入H.264裸流文件
|
||
"-skip_frame", "nokey", // 只处理关键帧(I帧)
|
||
"-vsync", "0", // 禁用帧同步,保证每个I帧都输出
|
||
"-q:v", "2", // 图片质量(1-31,值越小质量越高)
|
||
}
|
||
|
||
// 可选:指定分辨率(如果FFmpeg自动探测失败时使用)
|
||
if width > 0 && height > 0 {
|
||
args = append(args, "-s", fmt.Sprintf("%dx%d", width, height))
|
||
}
|
||
|
||
// 补全输出参数
|
||
args = append(args,
|
||
"-f", "image2", // 输出图片序列格式
|
||
outputPattern, // 输出文件模板
|
||
"-y", // 覆盖已存在的文件
|
||
)
|
||
|
||
// 创建命令并捕获输出
|
||
cmd := exec.Command("ffmpeg", args...)
|
||
_, err := cmd.StdoutPipe()
|
||
if err != nil {
|
||
return fmt.Errorf("创建标准输出管道失败: %v", err)
|
||
}
|
||
stderr, err := cmd.StderrPipe()
|
||
if err != nil {
|
||
return fmt.Errorf("创建标准错误管道失败: %v", err)
|
||
}
|
||
|
||
// 启动命令
|
||
if err := cmd.Start(); err != nil {
|
||
return fmt.Errorf("启动FFmpeg失败: %v", err)
|
||
}
|
||
|
||
// 实时打印FFmpeg日志(便于调试H.264解析问题)
|
||
scanner := bufio.NewScanner(stderr)
|
||
for scanner.Scan() {
|
||
line := scanner.Text()
|
||
fmt.Println("FFmpeg日志:", line)
|
||
// 捕获关键错误信息
|
||
if strings.Contains(line, "error") && strings.Contains(line, "H.264") {
|
||
fmt.Println("⚠️ H.264解析警告:", line)
|
||
}
|
||
}
|
||
|
||
// 等待命令执行完成
|
||
if err := cmd.Wait(); err != nil {
|
||
return fmt.Errorf("FFmpeg执行失败: %v", err)
|
||
}
|
||
|
||
// 统计输出的关键帧图片数量
|
||
files, err := filepath.Glob(filepath.Join(outputDir, "h264_keyframe_*."+format))
|
||
if err != nil {
|
||
return fmt.Errorf("统计输出图片失败: %v", err)
|
||
}
|
||
|
||
fmt.Printf("成功从H.264裸流提取 %d 个关键帧,保存至: %s\n", len(files), outputDir)
|
||
return nil
|
||
}
|