630 lines
21 KiB
Go
630 lines
21 KiB
Go
package video_server
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"FireLeave_tool/logger"
|
|
)
|
|
|
|
// 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] 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 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] Check video directory: %s (DeviceUUID=%s)", time.Since(startTime).Seconds(), videoDir, deviceUUID)
|
|
if _, err := os.Stat(videoDir); os.IsNotExist(err) {
|
|
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] 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] Video directory is ready: %s (DeviceUUID=%s)", time.Since(startTime).Seconds(), videoDir, deviceUUID)
|
|
|
|
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] Time aligned: alarmTime=%d aligned=%d (DeviceUUID=%s)", time.Since(startTime).Seconds(), alarmTime, alarmTimeAligned, deviceUUID)
|
|
|
|
beforeTime := alarmTimeAligned - 10
|
|
afterTime := alarmTimeAligned + 10
|
|
|
|
beforeStr := time.Unix(beforeTime, 0).Format("20060102150405")
|
|
afterStr := time.Unix(afterTime, 0).Format("20060102150405")
|
|
|
|
targetNames := []string{beforeStr, alarmTimeStr, afterStr}
|
|
|
|
videoFiles := []string{}
|
|
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(), filepath.Base(path), deviceUUID)
|
|
} else {
|
|
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)
|
|
}
|
|
|
|
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] 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] Read video directory failed: %v (DeviceUUID=%s)", time.Since(startTime).Seconds(), err, deviceUUID)
|
|
} else {
|
|
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() && isSupportedRawVideoFile(f.Name()) {
|
|
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] 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) {
|
|
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] Collected %d video files for merge (DeviceUUID=%s)", time.Since(startTime).Seconds(), len(videoFiles), deviceUUID)
|
|
if len(videoFiles) < 2 {
|
|
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))
|
|
}
|
|
|
|
logger.Logger.Printf("[%.3f] Sort video files by timestamp (DeviceUUID=%s)", time.Since(startTime).Seconds(), deviceUUID)
|
|
sort.Slice(videoFiles, func(i, j int) bool {
|
|
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)
|
|
for i, f := range videoFiles {
|
|
logger.Logger.Printf("[%.3f] [%d] %s (DeviceUUID=%s)", time.Since(startTime).Seconds(), i, filepath.Base(f), deviceUUID)
|
|
}
|
|
|
|
outputFile := fmt.Sprintf("%s.mp4_joined.mp4", alarmTimeStr)
|
|
joinedPath := filepath.Join(videoDir, outputFile)
|
|
logger.Logger.Printf("[%.3f] Output temp file: %s (DeviceUUID=%s)", time.Since(startTime).Seconds(), joinedPath, 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] Old output file removed: %s (DeviceUUID=%s)", time.Since(startTime).Seconds(), joinedPath, deviceUUID)
|
|
}
|
|
|
|
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] 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] 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] Merge command succeeded (elapsed=%.3fs) (DeviceUUID=%s)", time.Since(startTime).Seconds(), time.Since(mergeStartTime).Seconds(), 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] 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] 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)
|
|
|
|
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", uploadDeviceUUID, timestampPart)
|
|
finalPath := filepath.Join("/data/upload", finalName)
|
|
logger.Logger.Printf("[%.3f] Final upload path: %s (DeviceUUID=%s)", time.Since(startTime).Seconds(), finalPath, 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] 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] 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] 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] 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] Temp merged file deleted (DeviceUUID=%s)", time.Since(startTime).Seconds(), deviceUUID)
|
|
}
|
|
|
|
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 reports whether files include the given timestamp.mp(files []string, ts int64) bool {
|
|
func containsTimestamp(files []string, ts int64) bool {
|
|
for _, f := range files {
|
|
timestamp, err := getTimestampFromPath(f)
|
|
if err == nil && timestamp == ts {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
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
|
|
}, 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
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
/*
|
|
|
|
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)
|
|
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()
|
|
}
|
|
|
|
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, 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, error) {
|
|
filename := filepath.Base(filePath)
|
|
timestamp, err := parseSimpleTimestamp(filename)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
return timestamp, nil
|
|
}
|
|
|
|
// contains
|
|
func contains(slice []string, item string) bool {
|
|
for _, s := range slice {
|
|
if s == item {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// CleanupOldVideos
|
|
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
|
|
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"))
|
|
}
|
|
|
|
func cleanupRawVideosDirectory(rawDir string) {
|
|
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 // 娑撳秷绉存潻?0閺夆槄绱濇稉宥嗙閻? }
|
|
}
|
|
|
|
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])
|
|
}
|
|
}
|
|
}
|