初始提交
This commit is contained in:
@@ -0,0 +1,294 @@
|
||||
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 解析简单的时间戳文件名 (如: 20251029181740.mp4)
|
||||
func parseSimpleTimestamp(filename string) (int64, error) {
|
||||
// 移除文件扩展名
|
||||
nameWithoutExt := strings.TrimSuffix(filename, filepath.Ext(filename))
|
||||
|
||||
// 文件名应该就是14位时间戳
|
||||
if len(nameWithoutExt) != 14 {
|
||||
//return 0, fmt.Errorf("无效的时间戳格式: %s (期望14位数字)", nameWithoutExt)
|
||||
}
|
||||
|
||||
// 解析时间戳
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
package video_server
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"FireLeave_tool/logger"
|
||||
)
|
||||
|
||||
// UploadToNginx 上传视频到 Nginx 服务器,返回 URL
|
||||
func UploadToNginx(filePath string, eventParams map[string]any, uploadURL string) (string, error) {
|
||||
// === 1. 打开文件 ===
|
||||
file, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("open file failed: %w", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
// === 2. 创建表单 ===
|
||||
body := new(bytes.Buffer)
|
||||
writer := multipart.NewWriter(body)
|
||||
|
||||
// --- 上传文件 ---
|
||||
part, err := writer.CreateFormFile("file", filepath.Base(filePath))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("create form file failed: %w", err)
|
||||
}
|
||||
if _, err = io.Copy(part, file); err != nil {
|
||||
return "", fmt.Errorf("copy file failed: %w", err)
|
||||
}
|
||||
|
||||
// --- 上传 JSON ---
|
||||
eventJSON, err := json.Marshal(eventParams)
|
||||
if err != nil {
|
||||
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))
|
||||
|
||||
if err := writer.WriteField("event", string(eventJSON)); err != nil {
|
||||
return "", fmt.Errorf("write event field failed: %w", err)
|
||||
}
|
||||
|
||||
if err := writer.Close(); err != nil {
|
||||
return "", fmt.Errorf("close writer failed: %w", err)
|
||||
}
|
||||
|
||||
// === 3. 打印请求头 ===
|
||||
logger.Logger.Printf("[HTTP Request]URL: %s", uploadURL)
|
||||
logger.Logger.Printf("[HTTP Request]Content-Type: %s", writer.FormDataContentType())
|
||||
logger.Logger.Printf("[HTTP Request]Body 长度: %d bytes", body.Len())
|
||||
|
||||
// === 4. 发送请求 ===
|
||||
req, err := http.NewRequest("POST", uploadURL, body)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("create request failed: %w", err)
|
||||
}
|
||||
req.Header.Set("Content-Type", writer.FormDataContentType())
|
||||
|
||||
client := &http.Client{Timeout: 180 * time.Second}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("upload failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// === 5. 打印响应 ===
|
||||
respBody, _ := io.ReadAll(resp.Body)
|
||||
logger.Logger.Printf("[nginx response]Status: %d", resp.StatusCode)
|
||||
logger.Logger.Printf("[nginx response]Body: %s", string(respBody))
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return "", fmt.Errorf("nginx error %d: %s", resp.StatusCode, string(respBody))
|
||||
}
|
||||
|
||||
// === 6. 解析 video_url ===
|
||||
var result struct {
|
||||
Code int `json:"code"`
|
||||
Result json.RawMessage `json:"result"`
|
||||
}
|
||||
if err := json.Unmarshal(respBody, &result); err != nil {
|
||||
return "", fmt.Errorf("parse response failed: %w", err)
|
||||
}
|
||||
if result.Code != 0 {
|
||||
return "", fmt.Errorf("nginx error: %s", string(respBody))
|
||||
}
|
||||
|
||||
var videoURL string
|
||||
json.Unmarshal(result.Result, &videoURL)
|
||||
|
||||
logger.Logger.Printf("[ Upload Successful ] Local file: %s → Remote URL: %s", filePath, videoURL)
|
||||
return videoURL, nil
|
||||
}
|
||||
Reference in New Issue
Block a user