feat: update inference reporting and ffmpeg video merge

This commit is contained in:
gqc
2026-07-06 16:26:08 +08:00
parent 79490bd27b
commit 0842f555fd
10 changed files with 1430 additions and 990 deletions
+342 -175
View File
@@ -1,7 +1,6 @@
package video_server
package video_server
import (
"context"
"fmt"
"io"
"os"
@@ -15,35 +14,45 @@ import (
"FireLeave_tool/logger"
)
// MergeVideo 合并视频文件 - 完全适配 mp4_merge --out 模式
func MergeVideo(deviceUUID string, alarmTime int64) (string, error) {
// MergeVideo merges nearby raw video clips with ffmpeg.
func MergeVideo(deviceUUID string, alarmTime int64) (mergedPath string, err error) {
return mergeVideo(deviceUUID, deviceUUID, alarmTime)
}
// MergeVideoFromDirectory reads clips from one device directory while naming
// the uploaded file with the real device UUID.
func MergeVideoFromDirectory(videoDirectoryUUID, uploadDeviceUUID string, alarmTime int64) (mergedPath string, err error) {
return mergeVideo(videoDirectoryUUID, uploadDeviceUUID, alarmTime)
}
func mergeVideo(deviceUUID, uploadDeviceUUID string, alarmTime int64) (mergedPath string, err error) {
startTime := time.Now()
logger.Logger.Printf("[%.3f] 开始合并视频 (DeviceUUID=%s, alarmTime=%d)", time.Since(startTime).Seconds(), deviceUUID, alarmTime)
// 捕获所有异常,确保函数不会因为panic而崩溃
logger.Logger.Printf("[%.3f] Start video merge (DeviceUUID=%s, alarmTime=%d)", time.Since(startTime).Seconds(), deviceUUID, alarmTime)
// Guard the merge path so one bad video cannot crash the process.
defer func() {
if err := recover(); err != nil {
logger.Logger.Printf("[%.3f] 合并视频发生panic: %v (DeviceUUID=%s)", time.Since(startTime).Seconds(), err, deviceUUID)
if recovered := recover(); recovered != nil {
logger.Logger.Printf("[%.3f] Merge video panic: %v (DeviceUUID=%s)", time.Since(startTime).Seconds(), recovered, deviceUUID)
mergedPath = ""
err = fmt.Errorf("merge video panic: %v", recovered)
}
}()
// 检查视频目录
videoDir := fmt.Sprintf("/usr/data/camera/%s", deviceUUID)
logger.Logger.Printf("[%.3f] 检查视频目录: %s (DeviceUUID=%s)", time.Since(startTime).Seconds(), videoDir, deviceUUID)
logger.Logger.Printf("[%.3f] Check video directory: %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)
logger.Logger.Printf("[%.3f] Video directory does not exist: %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)
logger.Logger.Printf("[%.3f] Check video directory failed: %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)
logger.Logger.Printf("[%.3f] Video directory is ready: %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)
logger.Logger.Printf("[%.3f] Align alarm time to 10-second boundary (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)
logger.Logger.Printf("[%.3f] Time aligned: alarmTime=%d aligned=%d (DeviceUUID=%s)", time.Since(startTime).Seconds(), alarmTime, alarmTimeAligned, deviceUUID)
beforeTime := alarmTimeAligned - 10
afterTime := alarmTimeAligned + 10
@@ -51,40 +60,37 @@ func MergeVideo(deviceUUID string, alarmTime int64) (string, error) {
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"),
}
targetNames := []string{beforeStr, alarmTimeStr, afterStr}
// 查找视频文件
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 {
logger.Logger.Printf("[%.3f] Search expected video files (DeviceUUID=%s)", time.Since(startTime).Seconds(), deviceUUID)
missingVideos := []string{}
for _, name := range targetNames {
if path, ok := findVideoFileForTimestamp(videoDir, name); ok {
videoFiles = append(videoFiles, path)
logger.Logger.Printf("[%.3f] Found video: %s (DeviceUUID=%s)", time.Since(startTime).Seconds(), name+".mp4", deviceUUID)
logger.Logger.Printf("[%.3f] Found video: %s (DeviceUUID=%s)", time.Since(startTime).Seconds(), filepath.Base(path), deviceUUID)
} else {
logger.Logger.Printf("[%.3f] Missing video: %s (DeviceUUID=%s, error: %v)", time.Since(startTime).Seconds(), name+".mp4", deviceUUID, err)
missingVideos = append(missingVideos, name+".mp4/.h264")
}
}
if len(missingVideos) > 0 {
logger.Logger.Printf("[%.3f] Missing expected videos: %s (DeviceUUID=%s)", time.Since(startTime).Seconds(), strings.Join(missingVideos, ","), deviceUUID)
}
// === 2. 补充替代文件 ===
logger.Logger.Printf("[%.3f] 当前找到 %d 个视频文件,开始补充替代文件 (DeviceUUID=%s)", time.Since(startTime).Seconds(), len(videoFiles), deviceUUID)
logger.Logger.Printf("[%.3f] Found %d expected video files, searching fallbacks if needed (DeviceUUID=%s)", time.Since(startTime).Seconds(), len(videoFiles), deviceUUID)
if len(videoFiles) < 3 {
logger.Logger.Printf("[%.3f] 视频文件不足3个,开始查找替代文件 (DeviceUUID=%s)", time.Since(startTime).Seconds(), deviceUUID)
logger.Logger.Printf("[%.3f] Less than 3 expected videos found, scan directory for fallback files (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)
logger.Logger.Printf("[%.3f] Read video directory failed: %v (DeviceUUID=%s)", time.Since(startTime).Seconds(), err, deviceUUID)
} else {
logger.Logger.Printf("[%.3f] 读取目录所有文件,共 %d 个文件 (DeviceUUID=%s)", time.Since(startTime).Seconds(), len(allFiles), deviceUUID)
logger.Logger.Printf("[%.3f] Directory contains %d files (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 !f.IsDir() && isSupportedRawVideoFile(f.Name()) {
if ts, err := parseSimpleTimestamp(f.Name()); err == nil {
candidates = append(candidates, struct {
path string
@@ -93,7 +99,7 @@ func MergeVideo(deviceUUID string, alarmTime int64) (string, error) {
}
}
}
logger.Logger.Printf("[%.3f] 找到 %d 个候选视频文件 (DeviceUUID=%s)", time.Since(startTime).Seconds(), len(candidates), deviceUUID)
logger.Logger.Printf("[%.3f] Found %d fallback candidate files (DeviceUUID=%s)", time.Since(startTime).Seconds(), len(candidates), deviceUUID)
targets := []int64{beforeTime, alarmTimeAligned, afterTime}
for _, target := range targets {
if containsTimestamp(videoFiles, target) {
@@ -108,16 +114,20 @@ func MergeVideo(deviceUUID string, alarmTime int64) (string, error) {
}
}
logger.Logger.Printf("[%.3f] 最终收集到 %d 个视频文件 (DeviceUUID=%s)", time.Since(startTime).Seconds(), len(videoFiles), deviceUUID)
logger.Logger.Printf("[%.3f] Collected %d video files for merge (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)
logger.Logger.Printf("[%.3f] Not enough video files, need at least 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)
logger.Logger.Printf("[%.3f] Sort video files by timestamp (DeviceUUID=%s)", time.Since(startTime).Seconds(), deviceUUID)
sort.Slice(videoFiles, func(i, j int) bool {
return getTimestampFromPath(videoFiles[i]) < getTimestampFromPath(videoFiles[j])
left, leftErr := getTimestampFromPath(videoFiles[i])
right, rightErr := getTimestampFromPath(videoFiles[j])
if leftErr != nil || rightErr != nil {
return videoFiles[i] < videoFiles[j]
}
return left < right
})
logger.Logger.Printf("[%.3f] Merging %d videos (DeviceUUID=%s):", time.Since(startTime).Seconds(), len(videoFiles), deviceUUID)
@@ -125,93 +135,203 @@ func MergeVideo(deviceUUID string, alarmTime int64) (string, error) {
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] Output temp file: %s (DeviceUUID=%s)", time.Since(startTime).Seconds(), joinedPath, deviceUUID)
// 清理旧文件(防止冲突)
logger.Logger.Printf("[%.3f] 清理旧的输出文件 (DeviceUUID=%s)", time.Since(startTime).Seconds(), deviceUUID)
logger.Logger.Printf("[%.3f] Remove old output file if present (DeviceUUID=%s)", time.Since(startTime).Seconds(), deviceUUID)
if err := os.Remove(joinedPath); err != nil && !os.IsNotExist(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)
logger.Logger.Printf("[%.3f] Old output file removed: %s (DeviceUUID=%s)", time.Since(startTime).Seconds(), joinedPath, deviceUUID)
}
// === 5. 调用 mp4_merge,显式 --out ===
args := append(videoFiles, "--out", joinedPath)
cmd := exec.Command("mp4_merge", args...)
args := []string{"-hide_banner", "-loglevel", "error", "-y"}
if allH264Files(videoFiles) {
rawJoinedPath := filepath.Join(videoDir, fmt.Sprintf("%s_joined.h264", alarmTimeStr))
logger.Logger.Printf("[%.3f] Concatenate H264 files in timestamp order: %s (DeviceUUID=%s)", time.Since(startTime).Seconds(), rawJoinedPath, deviceUUID)
if err := concatenateFiles(rawJoinedPath, videoFiles); err != nil {
logger.Logger.Printf("[%.3f] Concatenate H264 files failed: %v (DeviceUUID=%s)", time.Since(startTime).Seconds(), err, deviceUUID)
return "", fmt.Errorf("concatenate H264 files failed: %v", err)
}
defer os.Remove(rawJoinedPath)
args = append(args,
"-fflags", "+genpts",
"-f", "h264",
"-i", rawJoinedPath,
"-c:v", "copy",
"-movflags", "+faststart",
joinedPath,
)
} else {
concatListPath := filepath.Join(videoDir, fmt.Sprintf("%s_ffmpeg_concat.txt", alarmTimeStr))
if err := writeFFmpegConcatList(concatListPath, videoFiles); err != nil {
logger.Logger.Printf("[%.3f] Create ffmpeg concat list failed: %v (DeviceUUID=%s)", time.Since(startTime).Seconds(), err, deviceUUID)
return "", fmt.Errorf("create ffmpeg concat list failed: %v", err)
}
defer os.Remove(concatListPath)
args = append(args,
"-f", "concat",
"-safe", "0",
"-i", concatListPath,
"-c", "copy",
"-movflags", "+faststart",
joinedPath,
)
}
cmd := exec.Command("ffmpeg", args...)
cmd.Env = ffmpegCleanEnv()
cmd.Stdout = logger.Logger.Writer()
cmd.Stderr = logger.Logger.Writer()
logger.Logger.Printf("[%.3f] 开始执行视频合并命令: %v (DeviceUUID=%s)", time.Since(startTime).Seconds(), cmd.Args, deviceUUID)
logger.Logger.Printf("[%.3f] Run merge command: %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] Merge command failed (elapsed=%.3fs): %v (DeviceUUID=%s)", time.Since(startTime).Seconds(), time.Since(mergeStartTime).Seconds(), err, deviceUUID)
return "", fmt.Errorf("ffmpeg merge failed: %v", err)
}
logger.Logger.Printf("[%.3f] 视频合并命令执行成功 (耗时:%.3fs) (DeviceUUID=%s)", time.Since(startTime).Seconds(), time.Since(mergeStartTime).Seconds(), deviceUUID)
logger.Logger.Printf("[%.3f] Merge command succeeded (elapsed=%.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)
logger.Logger.Printf("[%.3f] Verify merged file: %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)
logger.Logger.Printf("[%.3f] Output file not created: %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)
logger.Logger.Printf("[%.3f] Stat merged file failed: %v (DeviceUUID=%s)", time.Since(startTime).Seconds(), err, deviceUUID)
return "", fmt.Errorf("stat output file failed: %v", err)
}
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)
logger.Logger.Printf("[%.3f] Move merged file to /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)
finalName := fmt.Sprintf("%s-%s.MP4", uploadDeviceUUID, timestampPart)
finalPath := filepath.Join("/data/upload", finalName)
logger.Logger.Printf("[%.3f] 最终文件路径: %s (DeviceUUID=%s)", time.Since(startTime).Seconds(), finalPath, deviceUUID)
logger.Logger.Printf("[%.3f] Final upload path: %s (DeviceUUID=%s)", time.Since(startTime).Seconds(), finalPath, deviceUUID)
logger.Logger.Printf("[%.3f] 确保 /data/upload 目录存在 (DeviceUUID=%s)", time.Since(startTime).Seconds(), deviceUUID)
logger.Logger.Printf("[%.3f] Ensure /data/upload exists (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)
logger.Logger.Printf("[%.3f] Create /data/upload failed: %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)
logger.Logger.Printf("[%.3f] Copy merged file: %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)
logger.Logger.Printf("[%.3f] Copy merged file failed (elapsed=%.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)
logger.Logger.Printf("[%.3f] Copy merged file succeeded (elapsed=%.3fs) (DeviceUUID=%s)", time.Since(startTime).Seconds(), time.Since(copyStartTime).Seconds(), deviceUUID)
logger.Logger.Printf("[%.3f] Delete temp merged file: %s (DeviceUUID=%s)", time.Since(startTime).Seconds(), joinedPath, deviceUUID)
if err := os.Remove(joinedPath); err != nil {
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("[%.3f] Temp merged file deleted (DeviceUUID=%s)", time.Since(startTime).Seconds(), deviceUUID)
}
logger.Logger.Printf("[%.3f] Video merged and moved successfully: %s (DeviceUUID=%s, 总耗时:%.3fs)", time.Since(startTime).Seconds(), finalPath, deviceUUID, time.Since(startTime).Seconds())
logger.Logger.Printf("[%.3f] Video merged and moved successfully: %s (DeviceUUID=%s, total=%.3fs)", time.Since(startTime).Seconds(), finalPath, deviceUUID, time.Since(startTime).Seconds())
return finalPath, nil
}
// containsTimestamp 检查是否已有该时间戳
// containsTimestamp reports whether files include the given timestamp.mp(files []string, ts int64) bool {
func containsTimestamp(files []string, ts int64) bool {
for _, f := range files {
if getTimestampFromPath(f) == ts {
timestamp, err := getTimestampFromPath(f)
if err == nil && timestamp == ts {
return true
}
}
return false
}
// findClosest 查找最接近的时间戳文件
func findVideoFileForTimestamp(videoDir, timestamp string) (string, bool) {
for _, ext := range []string{".mp4", ".h264"} {
path := filepath.Join(videoDir, timestamp+ext)
if _, err := os.Stat(path); err == nil {
return path, true
}
}
return "", false
}
func isSupportedRawVideoFile(name string) bool {
if strings.Contains(name, "_joined") {
return false
}
ext := strings.ToLower(filepath.Ext(name))
return ext == ".mp4" || ext == ".h264"
}
func writeFFmpegConcatList(path string, videoFiles []string) error {
var builder strings.Builder
for _, videoFile := range videoFiles {
escaped := strings.ReplaceAll(videoFile, "'", "'\\''")
builder.WriteString("file '")
builder.WriteString(escaped)
builder.WriteString("'\n")
}
return os.WriteFile(path, []byte(builder.String()), 0644)
}
func allH264Files(videoFiles []string) bool {
for _, videoFile := range videoFiles {
if !strings.EqualFold(filepath.Ext(videoFile), ".h264") {
return false
}
}
return len(videoFiles) > 0
}
func concatenateFiles(outputPath string, inputPaths []string) (err error) {
output, err := os.Create(outputPath)
if err != nil {
return err
}
defer func() {
if closeErr := output.Close(); err == nil {
err = closeErr
}
if err != nil {
_ = os.Remove(outputPath)
}
}()
for _, inputPath := range inputPaths {
input, openErr := os.Open(inputPath)
if openErr != nil {
return fmt.Errorf("open %s: %w", inputPath, openErr)
}
_, copyErr := io.Copy(output, input)
closeErr := input.Close()
if copyErr != nil {
return fmt.Errorf("append %s: %w", inputPath, copyErr)
}
if closeErr != nil {
return fmt.Errorf("close %s: %w", inputPath, closeErr)
}
}
return output.Sync()
}
func ffmpegCleanEnv() []string {
env := os.Environ()
cleaned := make([]string, 0, len(env))
for _, item := range env {
if strings.HasPrefix(item, "LD_LIBRARY_PATH=") {
continue
}
cleaned = append(cleaned, item)
}
return cleaned
}
func findClosest(candidates []struct {
path string
ts int64
@@ -238,134 +358,182 @@ func abs(x int64) int64 {
return x
}
// copyFile 支持跨分区复制,带超时机制
func copyFile(src, dst string) error {
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
logger.Logger.Printf("[%.3f] Start file copy: %s -> %s", time.Since(startTime).Seconds(), src, dst)
in, err := os.Open(src)
if err != nil {
return err
}
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()
defer in.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()
// 获取源文件大小
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)
out, err := os.Create(dst)
if err != nil {
return err
}
logger.Logger.Printf("[%.3f] 文件复制操作完成", time.Since(startTime).Seconds())
defer out.Close()
deadline := time.Now().Add(30 * time.Second)
buf := make([]byte, 1024*1024)
var written int64
for {
if time.Now().After(deadline) {
_ = out.Close()
_ = os.Remove(dst)
logger.Logger.Printf("[%.3f] Copy timed out, removed incomplete file: %s", time.Since(startTime).Seconds(), dst)
return fmt.Errorf("copy file timeout after %d bytes", written)
}
nr, readErr := in.Read(buf)
if nr > 0 {
nw, writeErr := out.Write(buf[:nr])
written += int64(nw)
if writeErr != nil {
_ = os.Remove(dst)
return writeErr
}
if nw != nr {
_ = os.Remove(dst)
return io.ErrShortWrite
}
}
if readErr == io.EOF {
break
}
if readErr != nil {
_ = os.Remove(dst)
return readErr
}
}
if err := out.Sync(); err != nil {
_ = os.Remove(dst)
return err
}
logger.Logger.Printf("[%.3f] file copy completed: %d bytes", time.Since(startTime).Seconds(), written)
return nil
}
// alignTo10Seconds 将时间对齐到最近的10秒间隔
/*
func copyFile(src, dst string) error {
startTime := time.Now()
logger.Logger.Printf("[%.3f] Start file copy: %s -> %s", time.Since(startTime).Seconds(), src, dst)
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()
deadline := time.Now().Add(30 * time.Second)
buf := make([]byte, 1024*1024)
var written int64
for {
if time.Now().After(deadline) {
_ = out.Close()
_ = os.Remove(dst)
logger.Logger.Printf("[%.3f] Copy timed out, removed incomplete file: %s", time.Since(startTime).Seconds(), dst)
return fmt.Errorf("copy file timeout after %d bytes", written)
}
nr, readErr := in.Read(buf)
if nr > 0 {
nw, writeErr := out.Write(buf[:nr])
written += int64(nw)
if writeErr != nil {
_ = os.Remove(dst)
return writeErr
}
if nw != nr {
_ = os.Remove(dst)
return io.ErrShortWrite
}
}
if readErr == io.EOF {
break
}
if readErr != nil {
_ = os.Remove(dst)
return readErr
}
}
if err := out.Sync(); err != nil {
_ = os.Remove(dst)
return err
}
logger.Logger.Printf("[%.3f] file copy completed: %d bytes", time.Since(startTime).Seconds(), written)
return nil
}
// 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 {
return 0, fmt.Errorf("invalid timestamp filename length: %s", filename)
}
year, err := strconv.Atoi(nameWithoutExt[0:4])
// 解析时间戳
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])
// 鐟欙絾鐎介弮鍫曟?閹? year, err := strconv.Atoi(nameWithoutExt[0:4])
if err != nil {
return 0, fmt.Errorf("invalid year in filename %s: %w", filename, err)
}
month, err := strconv.Atoi(nameWithoutExt[4:6])
if err != nil {
return 0, fmt.Errorf("invalid month in filename %s: %w", filename, err)
}
day, err := strconv.Atoi(nameWithoutExt[6:8])
if err != nil {
return 0, fmt.Errorf("invalid day in filename %s: %w", filename, err)
}
hour, err := strconv.Atoi(nameWithoutExt[8:10])
if err != nil {
return 0, fmt.Errorf("invalid hour in filename %s: %w", filename, err)
}
minute, err := strconv.Atoi(nameWithoutExt[10:12])
if err != nil {
return 0, fmt.Errorf("invalid minute in filename %s: %w", filename, err)
}
second, err := strconv.Atoi(nameWithoutExt[12:14])
if err != nil {
return 0, fmt.Errorf("invalid second in filename %s: %w", filename, err)
}
t := time.Date(year, time.Month(month), day, hour, minute, second, 0, time.Local)
return t.Unix(), nil
}
// getTimestampFromPath 从文件路径中提取时间戳
func getTimestampFromPath(filePath string) int64 {
// getTimestampFromPath
func getTimestampFromPath(filePath string) (int64, error) {
filename := filepath.Base(filePath)
timestamp, err := parseSimpleTimestamp(filename)
if err != nil {
return 0
return 0, err
}
return timestamp
return timestamp, nil
}
// contains 检查切片是否包含某个元素
// contains
func contains(slice []string, item string) bool {
for _, s := range slice {
if s == item {
@@ -375,7 +543,7 @@ func contains(slice []string, item string) bool {
return false
}
// CleanupOldVideos 清理旧的合并视频文件,保留最近的 10 个
// CleanupOldVideos
func CleanupOldVideos(deviceUUID string) {
const maxFiles = 10
videoDir := "/data/upload"
@@ -414,16 +582,15 @@ func CleanupOldVideos(deviceUUID string) {
}
}
// CleanupOldRawVideos 清理推理程序生成的原始10s视频,保留最近60条
// CleanupOldRawVideos
func CleanupOldRawVideos(deviceUUID string) {
// 清理主设备目录
// 濞撳懐鎮婃稉鏄忣啎婢跺洨娲拌ぐ? cleanupRawVideosDirectory(filepath.Join("/usr/data/camera", deviceUUID))
cleanupRawVideosDirectory(filepath.Join("/usr/data/camera", deviceUUID))
// 清理火焰检测目录
// 濞撳懐鎮婇悘顐ゅ姍濡偓濞村娲拌ぐ? cleanupRawVideosDirectory(filepath.Join("/usr/data/camera", deviceUUID+"_fire_check"))
cleanupRawVideosDirectory(filepath.Join("/usr/data/camera", deviceUUID+"_fire_check"))
}
// cleanupRawVideosDirectory 清理单个目录中的原始视频文件
func cleanupRawVideosDirectory(rawDir string) {
if _, err := os.Stat(rawDir); os.IsNotExist(err) {
return
@@ -443,7 +610,7 @@ func cleanupRawVideosDirectory(rawDir string) {
}
if len(videoFiles) <= 60 {
return // 不超过60条,不清理
return // 娑撳秷绉存潻?0閺夆槄绱濇稉宥嗙閻? }
}
sort.Slice(videoFiles, func(i, j int) bool {