cctv/testdata/ffmpeg.d/cmd/prune/main.go

215 lines
5.0 KiB
Go

package main
import (
"context"
"fmt"
"io"
"log"
"math/big"
"os"
"os/exec"
"os/signal"
"path"
"slices"
"sort"
"strconv"
"strings"
"syscall"
"time"
)
func main() {
ctx, can := signal.NotifyContext(context.Background(), syscall.SIGINT)
defer can()
if err := Run(ctx, os.Args[1:]); err != nil {
panic(err)
}
}
func Run(ctx context.Context, args []string) error {
cams, err := lsd(args[0])
if err != nil {
return fmt.Errorf("failed to lsd %s: %w", args[0], err)
}
movementInterval, err := time.ParseDuration(args[1])
if err != nil {
return err
}
for _, cam := range cams {
files, err := lsf(cam)
if err != nil {
return fmt.Errorf("failed to lsf %s: %w", cam, err)
} else if len(files) < 1 {
continue
}
series := []string{}
for _, f := range files {
series = append(series, strings.Split(path.Base(f), ".")[0])
}
sort.Strings(series)
series = slices.Compact(series)
series = slices.DeleteFunc(series, func(s string) bool {
return path.Base(s) > time.Now().Add(-10*time.Minute).Format("2006-01-02T15-04-05.")
})
for _, series := range series {
if err := func() error {
seriesFiles := []string{}
for _, file := range files {
if strings.HasPrefix(path.Base(file), series) {
seriesFiles = append(seriesFiles, file)
}
}
if seriesHasMovement, err := seriesHasMovement(ctx, seriesFiles, movementInterval); err != nil {
return err
} else if seriesHasMovement {
log.Println("found movement in", series)
outd := strings.ReplaceAll(cam, "record", "movement")
os.MkdirAll(outd, os.ModePerm)
cmd := exec.CommandContext(ctx,
"ffmpeg",
"-y",
"-framerate", "3",
"-pattern_type", "glob",
"-i", path.Join(cam, series)+".*.jpg",
//"-filter_complex", "fps=3,scale=-1:-1:flags=lanczos[x];[x]split[x1][x2];[x1]palettegen[p];[x2][p]paletteuse",
"-r", "3",
path.Join(outd, series+".mp4"),
)
if out, err := cmd.CombinedOutput(); err != nil {
return fmt.Errorf("failed to persist series %s: (%w) %s", series, err, out)
}
} else {
log.Println("no movement in", series)
}
for _, seriesFile := range seriesFiles {
if err := os.Remove(seriesFile); err != nil {
return fmt.Errorf("failed to rm series %s[%s]: %w", series, seriesFile, err)
}
}
return nil
}(); err != nil {
return err
}
}
}
return nil
}
func seriesHasMovement(ctx context.Context, files []string, movementInterval time.Duration) (bool, error) {
if len(files) < 1 {
return false, nil
}
f := files[0]
fStat, err := os.Stat(f)
if err != nil {
return false, err
}
fTime := fStat.ModTime()
for i := 1; i < len(files); i++ {
g := files[i]
gStat, err := os.Stat(g)
if err != nil {
return false, err
}
gTime := gStat.ModTime()
if gTime.Sub(fTime) < movementInterval {
continue
}
if hasMovement, err := hasMovement(ctx, f, g); err != nil {
return false, fmt.Errorf("failed to check for movement between %s and %s: %w", f, g, err)
} else if hasMovement {
return true, nil
}
f = g
fTime = gTime
}
return false, nil
}
func hasMovement(ctx context.Context, a, b string) (bool, error) {
sizeOfCmd := exec.CommandContext(ctx, "identify", a)
sizeOfOutput, err := sizeOfCmd.CombinedOutput()
if err != nil {
return false, fmt.Errorf("failed to identify %s: (%w) %s", a, err, sizeOfOutput)
}
hw := strings.Fields(string(sizeOfOutput))[2]
h, err := strconv.ParseInt(strings.Split(hw, "x")[0], 10, 16)
if err != nil {
return false, fmt.Errorf("failed parsing %s for HxW: %w", hw, err)
}
w, err := strconv.ParseInt(strings.Split(hw, "x")[1], 10, 16)
if err != nil {
return false, fmt.Errorf("failed parsing %s for HxW: %w", hw, err)
}
total := h * w
compareCmd := exec.CommandContext(ctx, "compare", "-metric", "AE", "-fuzz", "15%", a, b, "/dev/null")
compareOutput, _ := compareCmd.CombinedOutput()
f, _, err := big.ParseFloat(string(compareOutput), 10, 0, big.ToNearestEven)
if err != nil {
return false, fmt.Errorf("failed to parse %s for a number of changed pixels: %w", compareOutput, err)
}
i := new(big.Int)
f.Int(i)
delta := i.Int64()
percentPixelsChanged := int(100.0 * float64(delta) / float64(total))
return percentPixelsChanged > 10, nil
}
func mv(wPath, rPath string) error {
r, err := os.Open(rPath)
if err != nil {
return err
}
defer r.Close()
os.MkdirAll(path.Dir(wPath), os.ModePerm)
w, err := os.Create(wPath)
if err != nil {
return err
}
defer w.Close()
if _, err := io.Copy(w, r); err != nil {
w.Close()
os.Remove(wPath)
return err
}
return os.Remove(rPath)
}
func lsd(d string) ([]string, error) {
return ls(d, true)
}
func lsf(d string) ([]string, error) {
return ls(d, false)
}
func ls(d string, dirs bool) ([]string, error) {
entries, err := os.ReadDir(d)
results := make([]string, 0, len(entries))
for i := range entries {
if dirs == entries[i].IsDir() {
results = append(results, path.Join(d, entries[i].Name()))
}
}
sort.Strings(results)
return results, err
}