2025-12-10 14:34:57 +08:00

333 lines
8.9 KiB
Go
Raw Permalink 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 video_server
import (
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
"sort"
"strconv"
"strings"
"time"
"FireLeave_tool/logger"
)
// MergeVideo 合并视频文件 - 完全适配 mp4_merge --out 模式
func MergeVideo(deviceUUID string, alarmTime int64) (string, error) {
videoDir := fmt.Sprintf("/usr/data/camera/%s", deviceUUID)
if _, err := os.Stat(videoDir); os.IsNotExist(err) {
return "", fmt.Errorf("video directory not exist: %s", videoDir)
}
// === 1. 时间对齐到 10 秒 ===
alarmTimeAligned := alignTo10Seconds(alarmTime)
alarmTimeStr := time.Unix(alarmTimeAligned, 0).Format("20060102150405")
beforeTime := alarmTimeAligned - 10
afterTime := alarmTimeAligned + 10
beforeStr := time.Unix(beforeTime, 0).Format("20060102150405")
afterStr := time.Unix(afterTime, 0).Format("20060102150405")
paths := map[string]string{
beforeStr: filepath.Join(videoDir, beforeStr+".mp4"),
alarmTimeStr: filepath.Join(videoDir, alarmTimeStr+".mp4"),
afterStr: filepath.Join(videoDir, afterStr+".mp4"),
}
videoFiles := []string{}
for name, path := range paths {
if _, err := os.Stat(path); err == nil {
videoFiles = append(videoFiles, path)
logger.Logger.Printf("Found video: %s", name+".mp4")
} else {
logger.Logger.Printf("Missing video: %s", name+".mp4")
}
}
// === 2. 补充替代文件 ===
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})
}
}
}
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))
}
}
}
if len(videoFiles) < 2 {
return "", fmt.Errorf("insufficient video files: %d", len(videoFiles))
}
// === 3. 排序 ===
sort.Slice(videoFiles, func(i, j int) bool {
return getTimestampFromPath(videoFiles[i]) < getTimestampFromPath(videoFiles[j])
})
logger.Logger.Printf("Merging %d videos:", len(videoFiles))
for i, f := range videoFiles {
logger.Logger.Printf(" [%d] %s", i, filepath.Base(f))
}
// === 4. 显式指定输出文件100% 可控)===
outputFile := fmt.Sprintf("%s.mp4_joined.mp4", alarmTimeStr)
joinedPath := filepath.Join(videoDir, outputFile)
// 清理旧文件(防止冲突)
if err := os.Remove(joinedPath); err != nil && !os.IsNotExist(err) {
logger.Logger.Printf("Failed to remove old output file %s: %v", joinedPath, err)
}
// === 5. 调用 mp4_merge显式 --out ===
args := append(videoFiles, "--out", joinedPath)
cmd := exec.Command("mp4_merge", args...)
cmd.Stdout = logger.Logger.Writer()
cmd.Stderr = logger.Logger.Writer()
if err := cmd.Run(); err != nil {
return "", fmt.Errorf("mp4_merge failed: %v", err)
}
// === 6. 验证输出文件存在 ===
if _, err := os.Stat(joinedPath); err != nil {
return "", fmt.Errorf("output file not created: %s, error: %v", joinedPath, err)
}
info, err := os.Stat(joinedPath)
if err != nil {
return "", fmt.Errorf("stat output file failed: %v", err)
}
logger.Logger.Printf("Merged file created: %s (size: %d bytes)", outputFile, info.Size())
// === 7. 移动到 /data/upload/ ===
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)
if err := os.MkdirAll("/data/upload", 0755); err != nil {
return "", fmt.Errorf("create /data/upload failed: %v", err)
}
// 跨分区复制 + 删除
if err := copyFile(joinedPath, finalPath); err != nil {
return "", fmt.Errorf("copy failed: %v", err)
}
if err := os.Remove(joinedPath); err != nil {
logger.Logger.Printf("Failed to delete temp file %s: %v", joinedPath, err)
}
logger.Logger.Printf("Video merged and moved successfully: %s", finalPath)
return finalPath, nil
}
// containsTimestamp 检查是否已有该时间戳
func containsTimestamp(files []string, ts int64) bool {
for _, f := range files {
if getTimestampFromPath(f) == ts {
return true
}
}
return false
}
// findClosest 查找最接近的时间戳文件
func findClosest(candidates []struct {
path string
ts int64
}, target int64) string {
var best string
var minDiff int64 = 1<<63 - 1
for _, c := range candidates {
diff := abs(c.ts - target)
if diff < minDiff {
minDiff = diff
best = c.path
}
}
if minDiff <= 30 {
return best
}
return ""
}
func abs(x int64) int64 {
if x < 0 {
return -x
}
return x
}
// copyFile 支持跨分区复制
func copyFile(src, dst string) error {
in, err := os.Open(src)
if err != nil {
return err
}
defer in.Close()
out, err := os.Create(dst)
if err != nil {
return err
}
defer out.Close()
if _, err = io.Copy(out, in); err != nil {
return err
}
return out.Sync()
}
// alignTo10Seconds 将时间对齐到最近的10秒间隔
func alignTo10Seconds(timestamp int64) int64 {
t := time.Unix(timestamp, 0)
// 获取秒数并对齐到10秒
seconds := t.Second()
alignedSeconds := (seconds / 10) * 10
// 创建新的时间
alignedTime := time.Date(t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), alignedSeconds, 0, t.Location())
return alignedTime.Unix()
}
// parseSimpleTimestamp 解析简单的时间戳文件名
func parseSimpleTimestamp(filename string) (int64, error) {
nameWithoutExt := strings.TrimSuffix(filename, filepath.Ext(filename))
if len(nameWithoutExt) != 14 {
}
// 解析时间戳
year, _ := strconv.Atoi(nameWithoutExt[0:4])
month, _ := strconv.Atoi(nameWithoutExt[4:6])
day, _ := strconv.Atoi(nameWithoutExt[6:8])
hour, _ := strconv.Atoi(nameWithoutExt[8:10])
minute, _ := strconv.Atoi(nameWithoutExt[10:12])
second, _ := strconv.Atoi(nameWithoutExt[12:14])
t := time.Date(year, time.Month(month), day, hour, minute, second, 0, time.Local)
return t.Unix(), nil
}
// getTimestampFromPath 从文件路径中提取时间戳
func getTimestampFromPath(filePath string) int64 {
filename := filepath.Base(filePath)
timestamp, err := parseSimpleTimestamp(filename)
if err != nil {
return 0
}
return timestamp
}
// contains 检查切片是否包含某个元素
func contains(slice []string, item string) bool {
for _, s := range slice {
if s == item {
return true
}
}
return false
}
// CleanupOldVideos 清理旧的合并视频文件,保留最近的 10 个
func CleanupOldVideos(deviceUUID string) {
const maxFiles = 10
videoDir := "/data/upload"
prefix := fmt.Sprintf("%s_", deviceUUID)
files, err := os.ReadDir(videoDir)
if err != nil {
logger.Logger.Printf("read the signals %s Error: %v", videoDir, err)
return
}
var videoFiles []os.DirEntry
for _, file := range files {
if strings.HasPrefix(file.Name(), prefix) && strings.HasSuffix(file.Name(), "_merged.mp4") {
videoFiles = append(videoFiles, file)
}
}
if len(videoFiles) <= maxFiles {
return
}
sort.Slice(videoFiles, func(i, j int) bool {
infoI, _ := videoFiles[i].Info()
infoJ, _ := videoFiles[j].Info()
return infoI.ModTime().Before(infoJ.ModTime())
})
for i := 0; i < len(videoFiles)-maxFiles; i++ {
filePath := filepath.Join(videoDir, videoFiles[i].Name())
if err := os.Remove(filePath); err != nil {
logger.Logger.Printf("Clean up old videos %s Error: %v", filePath, err)
} else {
logger.Logger.Printf("Clean up old videos: %s", filePath)
}
}
}
// CleanupOldRawVideos 清理推理程序生成的原始10s视频保留最近60条
func CleanupOldRawVideos(deviceUUID string) {
rawDir := filepath.Join("/usr/data/camera", deviceUUID)
if _, err := os.Stat(rawDir); os.IsNotExist(err) {
return
}
files, err := os.ReadDir(rawDir)
if err != nil {
logger.Logger.Printf("Failed to read the original video directory %s: %v", rawDir, err)
return
}
var videoFiles []string
for _, file := range files {
if !file.IsDir() && filepath.Ext(file.Name()) == ".mp4" {
videoFiles = append(videoFiles, filepath.Join(rawDir, file.Name()))
}
}
if len(videoFiles) <= 60 {
return // 不超过60条不清理
}
sort.Slice(videoFiles, func(i, j int) bool {
fi, _ := os.Stat(videoFiles[i])
fj, _ := os.Stat(videoFiles[j])
return fi.ModTime().Before(fj.ModTime())
})
for i := 0; i < len(videoFiles)-60; i++ {
if err := os.Remove(videoFiles[i]); err != nil {
logger.Logger.Printf("Failed to delete the old original video %s: %v", videoFiles[i], err)
} else {
logger.Logger.Printf("Clear the old original videos: %s", videoFiles[i])
}
}
}