219 lines
5.8 KiB
Go
219 lines
5.8 KiB
Go
package main
|
|||
|
|
|
||
|
|
import (
|
||
|
|
"context"
|
||
|
|
"flag"
|
||
|
|
"log"
|
||
|
|
"net"
|
||
|
|
"os"
|
||
|
|
"os/signal"
|
||
|
|
"sync"
|
||
|
|
"syscall"
|
||
|
|
"time"
|
||
|
|
|
||
|
|
"frame"
|
||
|
|
)
|
||
|
|
|
||
|
|
const (
|
||
|
|
workerCount = 4
|
||
|
|
|
||
|
|
// connLimit bounds concurrent inbound connections so a connection flood
|
||
|
|
// cannot exhaust goroutines or file descriptors.
|
||
|
|
connLimit = 16
|
||
|
|
|
||
|
|
// clipChanCap gives in-flight ingest handlers somewhere to park a decoded
|
||
|
|
// clip without holding the connection's socket open for the workers.
|
||
|
|
clipChanCap = 32
|
||
|
|
|
||
|
|
shutdownGrace = 5 * time.Second
|
||
|
|
)
|
||
|
|
|
||
|
|
func main() {
|
||
|
|
var (
|
||
|
|
listenAddr string
|
||
|
|
sendoffAddr string
|
||
|
|
classifierGlob string
|
||
|
|
motionPercent int
|
||
|
|
passphrase string
|
||
|
|
authToken string
|
||
|
|
pbkdf2Iter int
|
||
|
|
)
|
||
|
|
|
||
|
|
flag.StringVar(&listenAddr, "listen", ":8081", "address to listen for incoming clips")
|
||
|
|
flag.StringVar(&sendoffAddr, "sendoff", "localhost:8082", "address to forward classified clips")
|
||
|
|
flag.StringVar(&classifierGlob, "classifiers", "classifiers/*.xml", "glob pattern for Haar cascade XMLs")
|
||
|
|
flag.IntVar(&motionPercent, "motion-percent", 0, "minimum highest motion percent [0-100] required to classify and forward (0 = always classify)")
|
||
|
|
flag.StringVar(&passphrase, "passphrase", "", "decryption passphrase (empty = no encryption)")
|
||
|
|
flag.StringVar(&authToken, "auth-token", "", "shared pipeline auth token (or OKO_AUTH_TOKEN)")
|
||
|
|
flag.IntVar(&pbkdf2Iter, "pbkdf2-iters", frame.DefaultPBKDF2Iter, "PBKDF2 iterations for clip encryption (1..1000000)")
|
||
|
|
flag.Parse()
|
||
|
|
|
||
|
|
if pbkdf2Iter < 1 || pbkdf2Iter > frame.MaxPBKDF2Iter {
|
||
|
|
log.Fatalf("pbkdf2-iters must be in [1, %d], got %d", frame.MaxPBKDF2Iter, pbkdf2Iter)
|
||
|
|
}
|
||
|
|
if motionPercent < 0 || motionPercent > 100 {
|
||
|
|
log.Fatalf("motion-percent must be in [0, 100], got %d", motionPercent)
|
||
|
|
}
|
||
|
|
frame.SetDefaultPBKDF2Iter(pbkdf2Iter)
|
||
|
|
|
||
|
|
tok := authToken
|
||
|
|
if tok == "" {
|
||
|
|
tok = os.Getenv("OKO_AUTH_TOKEN")
|
||
|
|
}
|
||
|
|
if tok == "" {
|
||
|
|
log.Fatal("authentication required: set -auth-token or OKO_AUTH_TOKEN")
|
||
|
|
}
|
||
|
|
frame.SetAuthToken(tok)
|
||
|
|
|
||
|
|
// Load every classifier once at startup instead of per frame.
|
||
|
|
classifiers, err := frame.LoadClassifiers([]string{classifierGlob})
|
||
|
|
if err != nil {
|
||
|
|
log.Fatalf("LoadClassifiers: %v", err)
|
||
|
|
}
|
||
|
|
defer func() {
|
||
|
|
for i := range classifiers {
|
||
|
|
classifiers[i].Close()
|
||
|
|
}
|
||
|
|
}()
|
||
|
|
log.Printf("Loaded %d classifier(s)", len(classifiers))
|
||
|
|
|
||
|
|
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
||
|
|
defer cancel()
|
||
|
|
|
||
|
|
clipChan := make(chan frame.Clip, clipChanCap)
|
||
|
|
sendoffChan := make(chan frame.Clip, 8)
|
||
|
|
connSem := make(chan struct{}, connLimit)
|
||
|
|
|
||
|
|
ln, err := net.Listen("tcp", listenAddr)
|
||
|
|
if err != nil {
|
||
|
|
log.Fatalf("Listen %s: %v", listenAddr, err)
|
||
|
|
}
|
||
|
|
log.Printf("Listening on %s", listenAddr)
|
||
|
|
|
||
|
|
// Accept loop
|
||
|
|
var acceptWg sync.WaitGroup
|
||
|
|
rejected := frame.NewMalformedAttempts()
|
||
|
|
go func() {
|
||
|
|
<-ctx.Done()
|
||
|
|
ln.Close()
|
||
|
|
}()
|
||
|
|
go func() {
|
||
|
|
for {
|
||
|
|
conn, err := ln.Accept()
|
||
|
|
if err != nil {
|
||
|
|
if ctx.Err() != nil {
|
||
|
|
return
|
||
|
|
}
|
||
|
|
log.Printf("Accept error: %v", err)
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
select {
|
||
|
|
case connSem <- struct{}{}:
|
||
|
|
default:
|
||
|
|
log.Printf("Rejecting connection from %s: too many concurrent connections", conn.RemoteAddr())
|
||
|
|
conn.Close()
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
acceptWg.Add(1)
|
||
|
|
go func(c net.Conn) {
|
||
|
|
defer func() { <-connSem }()
|
||
|
|
defer acceptWg.Done()
|
||
|
|
frame.HandleClipConn(c, rejected, func(clp frame.Clip) {
|
||
|
|
clipChan <- clp
|
||
|
|
})
|
||
|
|
}(conn)
|
||
|
|
}
|
||
|
|
}()
|
||
|
|
|
||
|
|
// Bounded workers: motion-gate → classify → sendoffChan
|
||
|
|
var classifyWg sync.WaitGroup
|
||
|
|
classifyWg.Add(workerCount)
|
||
|
|
for w := 0; w < workerCount; w++ {
|
||
|
|
go func() {
|
||
|
|
defer classifyWg.Done()
|
||
|
|
for clp := range clipChan {
|
||
|
|
classifyClip(clp, sendoffChan, classifiers, motionPercent, passphrase)
|
||
|
|
}
|
||
|
|
}()
|
||
|
|
}
|
||
|
|
|
||
|
|
// Sendoff: forward processed clips to the coordinator with retry
|
||
|
|
var sendoffWg sync.WaitGroup
|
||
|
|
sendoffWg.Add(1)
|
||
|
|
go func() {
|
||
|
|
defer sendoffWg.Done()
|
||
|
|
for clp := range sendoffChan {
|
||
|
|
if err := clp.SendClipRetry(sendoffAddr, 6); err != nil {
|
||
|
|
log.Printf("Sendoff to %s failed, clip dropped: %v", sendoffAddr, err)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}()
|
||
|
|
|
||
|
|
<-ctx.Done()
|
||
|
|
log.Println("Shutting down...")
|
||
|
|
|
||
|
|
ln.Close()
|
||
|
|
if !frame.WaitGroupTimeout(&acceptWg, shutdownGrace) {
|
||
|
|
log.Println("Inbound handlers did not drain; closing channel anyway")
|
||
|
|
}
|
||
|
|
close(clipChan)
|
||
|
|
if !frame.WaitGroupTimeout(&classifyWg, shutdownGrace) {
|
||
|
|
log.Println("Classifiers still busy; closing sendoff anyway")
|
||
|
|
}
|
||
|
|
close(sendoffChan)
|
||
|
|
if !frame.WaitGroupTimeout(&sendoffWg, shutdownGrace) {
|
||
|
|
log.Println("Sendoff still busy (coordinator retry?); exiting anyway")
|
||
|
|
}
|
||
|
|
if n := rejected.Total(); n > 0 {
|
||
|
|
log.Printf("Rejected %d malformed clips from the network", n)
|
||
|
|
}
|
||
|
|
log.Println("Exiting")
|
||
|
|
}
|
||
|
|
|
||
|
|
// classifyClip decrypts, motion-gates (skipping recompute when mofin already
|
||
|
|
// did it), classifies, re-encrypts, and forwards the clip.
|
||
|
|
func classifyClip(clp frame.Clip, sendoff chan<- frame.Clip, classifiers []frame.Classifier, motionPercent int, passphrase string) {
|
||
|
|
if passphrase != "" {
|
||
|
|
if err := clp.Decrypt(passphrase); err != nil {
|
||
|
|
log.Printf("Decrypt error: %v", err)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if len(clp.Comparisons) == 0 {
|
||
|
|
if err := clp.CountChangedPixels(30); err != nil {
|
||
|
|
log.Printf("CountChangedPixels: %v", err)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
}
|
||
|
|
highest := clp.GetHighestMotion()
|
||
|
|
if motionPercent > 0 && highest < motionPercent {
|
||
|
|
log.Printf("Dropped clip with motion=%d%% (below %d%% threshold)", highest, motionPercent)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
frames := clp.Sublimate()
|
||
|
|
clp.Detections = make([]map[string][]frame.Detection, len(frames))
|
||
|
|
for i, frm := range frames {
|
||
|
|
frm.DetectClassifiers(classifiers)
|
||
|
|
clp.Detections[i] = frm.Detections
|
||
|
|
}
|
||
|
|
|
||
|
|
if passphrase != "" {
|
||
|
|
if err := clp.Encrypt(passphrase); err != nil {
|
||
|
|
log.Printf("Encrypt error: %v", err)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
hasDetection := false
|
||
|
|
for _, dets := range clp.Detections {
|
||
|
|
if len(dets) > 0 {
|
||
|
|
hasDetection = true
|
||
|
|
break
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
log.Printf("Forwarding clip with motion=%d%% (detections: %v)", highest, hasDetection)
|
||
|
|
sendoff <- clp
|
||
|
|
}
|