chore: upload current project state

This commit is contained in:
gqc
2026-05-12 15:45:50 +08:00
parent cd9b199e2c
commit 6056b1fdc5
25 changed files with 4051 additions and 373 deletions
+174 -44
View File
@@ -1,6 +1,7 @@
package video_server
import (
"context"
"fmt"
"io"
"os"
@@ -16,14 +17,33 @@ import (
// MergeVideo 合并视频文件 - 完全适配 mp4_merge --out 模式
func MergeVideo(deviceUUID string, alarmTime int64) (string, error) {
startTime := time.Now()
logger.Logger.Printf("[%.3f] 开始合并视频 (DeviceUUID=%s, alarmTime=%d)", time.Since(startTime).Seconds(), deviceUUID, alarmTime)
// 捕获所有异常,确保函数不会因为panic而崩溃
defer func() {
if err := recover(); err != nil {
logger.Logger.Printf("[%.3f] 合并视频发生panic: %v (DeviceUUID=%s)", time.Since(startTime).Seconds(), err, deviceUUID)
}
}()
// 检查视频目录
videoDir := fmt.Sprintf("/usr/data/camera/%s", deviceUUID)
logger.Logger.Printf("[%.3f] 检查视频目录: %s (DeviceUUID=%s)", time.Since(startTime).Seconds(), videoDir, deviceUUID)
if _, err := os.Stat(videoDir); os.IsNotExist(err) {
logger.Logger.Printf("[%.3f] 视频目录不存在: %s (DeviceUUID=%s)", time.Since(startTime).Seconds(), videoDir, deviceUUID)
return "", fmt.Errorf("video directory not exist: %s", videoDir)
} else if err != nil {
logger.Logger.Printf("[%.3f] 检查视频目录失败: %v (DeviceUUID=%s)", time.Since(startTime).Seconds(), err, deviceUUID)
return "", fmt.Errorf("check video directory failed: %v", err)
}
logger.Logger.Printf("[%.3f] 视频目录检查成功: %s (DeviceUUID=%s)", time.Since(startTime).Seconds(), videoDir, deviceUUID)
// === 1. 时间对齐到 10 秒 ===
logger.Logger.Printf("[%.3f] 开始时间对齐处理 (DeviceUUID=%s, alarmTime=%d)", time.Since(startTime).Seconds(), deviceUUID, alarmTime)
alarmTimeAligned := alignTo10Seconds(alarmTime)
alarmTimeStr := time.Unix(alarmTimeAligned, 0).Format("20060102150405")
logger.Logger.Printf("[%.3f] 时间对齐结果: alarmTime=%d → aligned=%d (DeviceUUID=%s)", time.Since(startTime).Seconds(), alarmTime, alarmTimeAligned, deviceUUID)
beforeTime := alarmTimeAligned - 10
afterTime := alarmTimeAligned + 10
@@ -37,67 +57,85 @@ func MergeVideo(deviceUUID string, alarmTime int64) (string, error) {
afterStr: filepath.Join(videoDir, afterStr+".mp4"),
}
// 查找视频文件
videoFiles := []string{}
logger.Logger.Printf("[%.3f] 开始查找视频文件 (DeviceUUID=%s)", time.Since(startTime).Seconds(), deviceUUID)
for name, path := range paths {
logger.Logger.Printf("[%.3f] 检查文件: %s (DeviceUUID=%s)", time.Since(startTime).Seconds(), path, deviceUUID)
if _, err := os.Stat(path); err == nil {
videoFiles = append(videoFiles, path)
logger.Logger.Printf("Found video: %s", name+".mp4")
logger.Logger.Printf("[%.3f] Found video: %s (DeviceUUID=%s)", time.Since(startTime).Seconds(), name+".mp4", deviceUUID)
} else {
logger.Logger.Printf("Missing video: %s", name+".mp4")
logger.Logger.Printf("[%.3f] Missing video: %s (DeviceUUID=%s, error: %v)", time.Since(startTime).Seconds(), name+".mp4", deviceUUID, err)
}
}
// === 2. 补充替代文件 ===
logger.Logger.Printf("[%.3f] 当前找到 %d 个视频文件,开始补充替代文件 (DeviceUUID=%s)", time.Since(startTime).Seconds(), len(videoFiles), deviceUUID)
if len(videoFiles) < 3 {
allFiles, _ := os.ReadDir(videoDir)
var candidates []struct {
path string
ts int64
}
for _, f := range allFiles {
if !f.IsDir() && strings.HasSuffix(f.Name(), ".mp4") && !strings.Contains(f.Name(), "_joined") {
if ts, err := parseSimpleTimestamp(f.Name()); err == nil {
candidates = append(candidates, struct {
path string
ts int64
}{filepath.Join(videoDir, f.Name()), ts})
logger.Logger.Printf("[%.3f] 视频文件不足3个,开始查找替代文件 (DeviceUUID=%s)", time.Since(startTime).Seconds(), deviceUUID)
allFiles, err := os.ReadDir(videoDir)
if err != nil {
logger.Logger.Printf("[%.3f] 读取目录失败: %v (DeviceUUID=%s)", time.Since(startTime).Seconds(), err, deviceUUID)
} else {
logger.Logger.Printf("[%.3f] 读取目录所有文件,共 %d 个文件 (DeviceUUID=%s)", time.Since(startTime).Seconds(), len(allFiles), deviceUUID)
var candidates []struct {
path string
ts int64
}
for _, f := range allFiles {
if !f.IsDir() && strings.HasSuffix(f.Name(), ".mp4") && !strings.Contains(f.Name(), "_joined") {
if ts, err := parseSimpleTimestamp(f.Name()); err == nil {
candidates = append(candidates, struct {
path string
ts int64
}{filepath.Join(videoDir, f.Name()), ts})
}
}
}
}
targets := []int64{beforeTime, alarmTimeAligned, afterTime}
for _, target := range targets {
if containsTimestamp(videoFiles, target) {
continue
}
closest := findClosest(candidates, target)
if closest != "" && !contains(videoFiles, closest) {
videoFiles = append(videoFiles, closest)
logger.Logger.Printf("Using fallback: %s", filepath.Base(closest))
logger.Logger.Printf("[%.3f] 找到 %d 个候选视频文件 (DeviceUUID=%s)", time.Since(startTime).Seconds(), len(candidates), deviceUUID)
targets := []int64{beforeTime, alarmTimeAligned, afterTime}
for _, target := range targets {
if containsTimestamp(videoFiles, target) {
continue
}
closest := findClosest(candidates, target)
if closest != "" && !contains(videoFiles, closest) {
videoFiles = append(videoFiles, closest)
logger.Logger.Printf("[%.3f] Using fallback: %s (DeviceUUID=%s)", time.Since(startTime).Seconds(), filepath.Base(closest), deviceUUID)
}
}
}
}
logger.Logger.Printf("[%.3f] 最终收集到 %d 个视频文件 (DeviceUUID=%s)", time.Since(startTime).Seconds(), len(videoFiles), deviceUUID)
if len(videoFiles) < 2 {
logger.Logger.Printf("[%.3f] 视频文件不足,至少需要 2 个文件 (DeviceUUID=%s)", time.Since(startTime).Seconds(), deviceUUID)
return "", fmt.Errorf("insufficient video files: %d", len(videoFiles))
}
// === 3. 排序 ===
logger.Logger.Printf("[%.3f] 开始排序视频文件 (DeviceUUID=%s)", time.Since(startTime).Seconds(), deviceUUID)
sort.Slice(videoFiles, func(i, j int) bool {
return getTimestampFromPath(videoFiles[i]) < getTimestampFromPath(videoFiles[j])
})
logger.Logger.Printf("Merging %d videos:", len(videoFiles))
logger.Logger.Printf("[%.3f] Merging %d videos (DeviceUUID=%s):", time.Since(startTime).Seconds(), len(videoFiles), deviceUUID)
for i, f := range videoFiles {
logger.Logger.Printf(" [%d] %s", i, filepath.Base(f))
logger.Logger.Printf("[%.3f] [%d] %s (DeviceUUID=%s)", time.Since(startTime).Seconds(), i, filepath.Base(f), deviceUUID)
}
// === 4. 显式指定输出文件(100% 可控)===
outputFile := fmt.Sprintf("%s.mp4_joined.mp4", alarmTimeStr)
joinedPath := filepath.Join(videoDir, outputFile)
logger.Logger.Printf("[%.3f] 输出文件路径: %s (DeviceUUID=%s)", time.Since(startTime).Seconds(), joinedPath, deviceUUID)
// 清理旧文件(防止冲突)
logger.Logger.Printf("[%.3f] 清理旧的输出文件 (DeviceUUID=%s)", time.Since(startTime).Seconds(), deviceUUID)
if err := os.Remove(joinedPath); err != nil && !os.IsNotExist(err) {
logger.Logger.Printf("Failed to remove old output file %s: %v", joinedPath, err)
logger.Logger.Printf("[%.3f] Failed to remove old output file %s: %v (DeviceUUID=%s)", time.Since(startTime).Seconds(), joinedPath, err, deviceUUID)
} else if err == nil {
logger.Logger.Printf("[%.3f] 旧的输出文件清理成功: %s (DeviceUUID=%s)", time.Since(startTime).Seconds(), joinedPath, deviceUUID)
}
// === 5. 调用 mp4_merge,显式 --out ===
@@ -105,42 +143,61 @@ func MergeVideo(deviceUUID string, alarmTime int64) (string, error) {
cmd := exec.Command("mp4_merge", args...)
cmd.Stdout = logger.Logger.Writer()
cmd.Stderr = logger.Logger.Writer()
logger.Logger.Printf("[%.3f] 开始执行视频合并命令: %v (DeviceUUID=%s)", time.Since(startTime).Seconds(), cmd.Args, deviceUUID)
mergeStartTime := time.Now()
if err := cmd.Run(); err != nil {
logger.Logger.Printf("[%.3f] 视频合并命令执行失败 (耗时:%.3fs): %v (DeviceUUID=%s)", time.Since(startTime).Seconds(), time.Since(mergeStartTime).Seconds(), err, deviceUUID)
return "", fmt.Errorf("mp4_merge failed: %v", err)
}
logger.Logger.Printf("[%.3f] 视频合并命令执行成功 (耗时:%.3fs) (DeviceUUID=%s)", time.Since(startTime).Seconds(), time.Since(mergeStartTime).Seconds(), deviceUUID)
// === 6. 验证输出文件存在 ===
logger.Logger.Printf("[%.3f] 验证输出文件是否存在: %s (DeviceUUID=%s)", time.Since(startTime).Seconds(), joinedPath, deviceUUID)
if _, err := os.Stat(joinedPath); err != nil {
logger.Logger.Printf("[%.3f] 输出文件未创建: %s, error: %v (DeviceUUID=%s)", time.Since(startTime).Seconds(), joinedPath, err, deviceUUID)
return "", fmt.Errorf("output file not created: %s, error: %v", joinedPath, err)
}
info, err := os.Stat(joinedPath)
if err != nil {
logger.Logger.Printf("[%.3f] 获取输出文件信息失败: %v (DeviceUUID=%s)", time.Since(startTime).Seconds(), err, deviceUUID)
return "", fmt.Errorf("stat output file failed: %v", err)
}
logger.Logger.Printf("Merged file created: %s (size: %d bytes)", outputFile, info.Size())
logger.Logger.Printf("[%.3f] Merged file created: %s (size: %d bytes) (DeviceUUID=%s)", time.Since(startTime).Seconds(), outputFile, info.Size(), deviceUUID)
// === 7. 移动到 /data/upload/ ===
logger.Logger.Printf("[%.3f] 开始移动文件到 /data/upload/ 目录 (DeviceUUID=%s)", time.Since(startTime).Seconds(), deviceUUID)
t := time.Unix(alarmTimeAligned, 0)
year, month, day := t.Date()
timestampPart := fmt.Sprintf("%d-%d-%d-%d", year, int(month), day, alarmTimeAligned)
finalName := fmt.Sprintf("%s-%s.MP4", deviceUUID, timestampPart)
finalPath := filepath.Join("/data/upload", finalName)
logger.Logger.Printf("[%.3f] 最终文件路径: %s (DeviceUUID=%s)", time.Since(startTime).Seconds(), finalPath, deviceUUID)
logger.Logger.Printf("[%.3f] 确保 /data/upload 目录存在 (DeviceUUID=%s)", time.Since(startTime).Seconds(), deviceUUID)
if err := os.MkdirAll("/data/upload", 0755); err != nil {
logger.Logger.Printf("[%.3f] 创建 /data/upload 目录失败: %v (DeviceUUID=%s)", time.Since(startTime).Seconds(), err, deviceUUID)
return "", fmt.Errorf("create /data/upload failed: %v", err)
}
// 跨分区复制 + 删除
logger.Logger.Printf("[%.3f] 开始复制文件: %s → %s (DeviceUUID=%s)", time.Since(startTime).Seconds(), joinedPath, finalPath, deviceUUID)
copyStartTime := time.Now()
if err := copyFile(joinedPath, finalPath); err != nil {
logger.Logger.Printf("[%.3f] 文件复制失败 (耗时:%.3fs): %v (DeviceUUID=%s)", time.Since(startTime).Seconds(), time.Since(copyStartTime).Seconds(), err, deviceUUID)
return "", fmt.Errorf("copy failed: %v", err)
}
logger.Logger.Printf("[%.3f] 文件复制成功 (耗时:%.3fs) (DeviceUUID=%s)", time.Since(startTime).Seconds(), time.Since(copyStartTime).Seconds(), deviceUUID)
logger.Logger.Printf("[%.3f] 删除临时文件: %s (DeviceUUID=%s)", time.Since(startTime).Seconds(), joinedPath, deviceUUID)
if err := os.Remove(joinedPath); err != nil {
logger.Logger.Printf("Failed to delete temp file %s: %v", joinedPath, err)
logger.Logger.Printf("[%.3f] Failed to delete temp file %s: %v (DeviceUUID=%s)", time.Since(startTime).Seconds(), joinedPath, err, deviceUUID)
} else {
logger.Logger.Printf("[%.3f] 临时文件删除成功 (DeviceUUID=%s)", time.Since(startTime).Seconds(), deviceUUID)
}
logger.Logger.Printf("Video merged and moved successfully: %s", finalPath)
logger.Logger.Printf("[%.3f] Video merged and moved successfully: %s (DeviceUUID=%s, 总耗时:%.3fs)", time.Since(startTime).Seconds(), finalPath, deviceUUID, time.Since(startTime).Seconds())
return finalPath, nil
}
@@ -181,24 +238,89 @@ func abs(x int64) int64 {
return x
}
// copyFile 支持跨分区复制
// copyFile 支持跨分区复制,带超时机制
func copyFile(src, dst string) error {
in, err := os.Open(src)
if err != nil {
return err
startTime := time.Now()
logger.Logger.Printf("[%.3f] 开始复制文件: %s → %s", time.Since(startTime).Seconds(), src, dst)
// 创建带超时的上下文
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
// 创建通道用于接收复制结果
type copyResult struct {
n int64
err error
}
defer in.Close()
resultChan := make(chan copyResult, 1)
// 在goroutine中执行复制操作
go func() {
// 打开源文件
logger.Logger.Printf("[%.3f] 打开源文件: %s", time.Since(startTime).Seconds(), src)
in, err := os.Open(src)
if err != nil {
resultChan <- copyResult{0, err}
return
}
defer in.Close()
out, err := os.Create(dst)
if err != nil {
return err
}
defer out.Close()
// 创建目标文件
logger.Logger.Printf("[%.3f] 创建目标文件: %s", time.Since(startTime).Seconds(), dst)
out, err := os.Create(dst)
if err != nil {
resultChan <- copyResult{0, err}
return
}
defer out.Close()
if _, err = io.Copy(out, in); err != nil {
return err
// 获取源文件大小
fileInfo, err := in.Stat()
if err != nil {
logger.Logger.Printf("[%.3f] 获取源文件信息失败: %v", time.Since(startTime).Seconds(), err)
} else {
logger.Logger.Printf("[%.3f] 源文件大小: %d 字节", time.Since(startTime).Seconds(), fileInfo.Size())
}
// 执行复制
logger.Logger.Printf("[%.3f] 开始执行文件复制操作", time.Since(startTime).Seconds())
copyStartTime := time.Now()
n, err := io.Copy(out, in)
elapsed := time.Since(copyStartTime)
if err != nil {
logger.Logger.Printf("[%.3f] 文件复制失败: %v, 已复制 %d 字节, 耗时 %v", time.Since(startTime).Seconds(), err, n, elapsed)
resultChan <- copyResult{n, err}
return
}
logger.Logger.Printf("[%.3f] 文件复制成功: 复制 %d 字节, 耗时 %v", time.Since(startTime).Seconds(), n, elapsed)
// 同步文件到磁盘
logger.Logger.Printf("[%.3f] 开始同步文件到磁盘", time.Since(startTime).Seconds())
syncStartTime := time.Now()
if err = out.Sync(); err != nil {
logger.Logger.Printf("[%.3f] 文件同步失败: %v, 耗时 %v", time.Since(startTime).Seconds(), err, time.Since(syncStartTime))
resultChan <- copyResult{n, err}
return
}
logger.Logger.Printf("[%.3f] 文件同步成功, 耗时 %v", time.Since(startTime).Seconds(), time.Since(syncStartTime))
resultChan <- copyResult{n, nil}
}()
// 等待复制结果或超时
select {
case result := <-resultChan:
if result.err != nil {
return result.err
}
case <-ctx.Done():
err := ctx.Err()
logger.Logger.Printf("[%.3f] 文件复制操作超时: %v", time.Since(startTime).Seconds(), err)
return fmt.Errorf("copy file timeout: %v", err)
}
return out.Sync()
logger.Logger.Printf("[%.3f] 文件复制操作完成", time.Since(startTime).Seconds())
return nil
}
// alignTo10Seconds 将时间对齐到最近的10秒间隔
@@ -294,7 +416,15 @@ func CleanupOldVideos(deviceUUID string) {
// CleanupOldRawVideos 清理推理程序生成的原始10s视频,保留最近60条
func CleanupOldRawVideos(deviceUUID string) {
rawDir := filepath.Join("/usr/data/camera", deviceUUID)
// 清理主设备目录
cleanupRawVideosDirectory(filepath.Join("/usr/data/camera", deviceUUID))
// 清理火焰检测目录
cleanupRawVideosDirectory(filepath.Join("/usr/data/camera", deviceUUID+"_fire_check"))
}
// cleanupRawVideosDirectory 清理单个目录中的原始视频文件
func cleanupRawVideosDirectory(rawDir string) {
if _, err := os.Stat(rawDir); os.IsNotExist(err) {
return
}
+50 -1
View File
@@ -16,36 +16,62 @@ import (
// UploadToNginx 上传视频到 Nginx 服务器,返回 URL
func UploadToNginx(filePath string, eventParams map[string]any, uploadURL string) (string, error) {
logger.Logger.Printf("开始上传视频到 Nginx: %s → %s", filePath, uploadURL)
// 打开文件
logger.Logger.Printf("打开文件: %s", filePath)
startTime := time.Now()
file, err := os.Open(filePath)
if err != nil {
logger.Logger.Printf("打开文件失败: %v, 耗时: %v", err, time.Since(startTime))
return "", fmt.Errorf("open file failed: %w", err)
}
defer file.Close()
logger.Logger.Printf("文件打开成功, 耗时: %v", time.Since(startTime))
// 创建多部分表单
logger.Logger.Printf("创建多部分表单")
body := new(bytes.Buffer)
writer := multipart.NewWriter(body)
// 创建表单文件字段
logger.Logger.Printf("创建表单文件字段: %s", filepath.Base(filePath))
part, err := writer.CreateFormFile("file", filepath.Base(filePath))
if err != nil {
logger.Logger.Printf("创建表单文件字段失败: %v", err)
return "", fmt.Errorf("create form file failed: %w", err)
}
// 复制文件内容到表单
logger.Logger.Printf("开始复制文件内容到表单")
copyStartTime := time.Now()
if _, err = io.Copy(part, file); err != nil {
logger.Logger.Printf("复制文件内容失败: %v, 耗时: %v", err, time.Since(copyStartTime))
return "", fmt.Errorf("copy file failed: %w", err)
}
logger.Logger.Printf("文件内容复制成功, 耗时: %v", time.Since(copyStartTime))
// 序列化事件参数
logger.Logger.Printf("序列化事件参数")
eventJSON, err := json.Marshal(eventParams)
if err != nil {
logger.Logger.Printf("序列化事件参数失败: %v", err)
return "", fmt.Errorf("marshal event failed: %w", err)
}
logger.Logger.Printf("[Upload Event parameters]eventParams: %v", eventParams)
logger.Logger.Printf("[Upload Event parameters]eventJSON: %s", string(eventJSON))
// 写入事件字段
logger.Logger.Printf("写入事件字段到表单")
if err := writer.WriteField("event", string(eventJSON)); err != nil {
logger.Logger.Printf("写入事件字段失败: %v", err)
return "", fmt.Errorf("write event field failed: %w", err)
}
// 关闭表单写入器
logger.Logger.Printf("关闭表单写入器")
if err := writer.Close(); err != nil {
logger.Logger.Printf("关闭表单写入器失败: %v", err)
return "", fmt.Errorf("close writer failed: %w", err)
}
@@ -53,41 +79,64 @@ func UploadToNginx(filePath string, eventParams map[string]any, uploadURL string
logger.Logger.Printf("[HTTP Request]Content-Type: %s", writer.FormDataContentType())
logger.Logger.Printf("[HTTP Request]Body 长度: %d bytes", body.Len())
// 创建HTTP请求
logger.Logger.Printf("创建HTTP请求")
req, err := http.NewRequest("POST", uploadURL, body)
if err != nil {
logger.Logger.Printf("创建HTTP请求失败: %v", err)
return "", fmt.Errorf("create request failed: %w", err)
}
req.Header.Set("Content-Type", writer.FormDataContentType())
// 发送HTTP请求
logger.Logger.Printf("发送HTTP请求到 Nginx")
httpStartTime := time.Now()
client := &http.Client{Timeout: 180 * time.Second}
resp, err := client.Do(req)
if err != nil {
logger.Logger.Printf("发送HTTP请求失败: %v, 耗时: %v", err, time.Since(httpStartTime))
return "", fmt.Errorf("upload failed: %w", err)
}
defer resp.Body.Close()
logger.Logger.Printf("HTTP请求发送成功, 耗时: %v", time.Since(httpStartTime))
// 读取响应
logger.Logger.Printf("读取HTTP响应")
respStartTime := time.Now()
respBody, _ := io.ReadAll(resp.Body)
logger.Logger.Printf("响应读取完成, 耗时: %v", time.Since(respStartTime))
logger.Logger.Printf("[nginx response]Status: %d", resp.StatusCode)
logger.Logger.Printf("[nginx response]Body: %s", string(respBody))
if resp.StatusCode != http.StatusOK {
logger.Logger.Printf("Nginx返回错误状态码: %d", resp.StatusCode)
return "", fmt.Errorf("nginx error %d: %s", resp.StatusCode, string(respBody))
}
// 解析响应
logger.Logger.Printf("解析Nginx响应")
var result struct {
Code int `json:"code"`
Result json.RawMessage `json:"result"`
}
if err := json.Unmarshal(respBody, &result); err != nil {
logger.Logger.Printf("解析响应失败: %v", err)
return "", fmt.Errorf("parse response failed: %w", err)
}
if result.Code != 0 {
logger.Logger.Printf("Nginx返回错误代码: %d", result.Code)
return "", fmt.Errorf("nginx error: %s", string(respBody))
}
// 提取视频URL
logger.Logger.Printf("提取视频URL")
var videoURL string
json.Unmarshal(result.Result, &videoURL)
if err := json.Unmarshal(result.Result, &videoURL); err != nil {
logger.Logger.Printf("提取视频URL失败: %v", err)
return "", fmt.Errorf("extract video URL failed: %w", err)
}
logger.Logger.Printf("[ Upload Successful ] Local file: %s → Remote URL: %s", filePath, videoURL)
logger.Logger.Printf("视频上传完成, 总耗时: %v", time.Since(startTime))
return videoURL, nil
}