Commit e5ee25e

bryfry <bryon@fryer.io>
2025-12-23 18:58:57
ioc
1 parent ed98913
Changed files (3)
internal/speco/speco.go
@@ -0,0 +1,90 @@
+package speco
+
+import (
+	"context"
+	"fmt"
+	"io"
+	"log/slog"
+	"net/http"
+	"net/url"
+	"time"
+)
+
+type Speco struct {
+	url     *url.URL
+	timeout time.Duration
+}
+
+type JPEG []byte
+
+func NewCamera(
+	url *url.URL,
+	timeout time.Duration,
+) Speco {
+	return Speco{
+		url:     url,
+		timeout: timeout,
+	}
+}
+
+func (s Speco) Snapshot(w io.Writer) (int64, error) {
+	client := &http.Client{
+		Timeout: s.timeout,
+	}
+
+	resp, err := client.Get(s.url.String())
+	if err != nil {
+		if urlError, ok := err.(*url.Error); ok && urlError.Timeout() {
+			return 0, err
+		}
+		return 0, err
+	}
+	defer resp.Body.Close()
+
+	if resp.StatusCode != http.StatusOK {
+		return 0, fmt.Errorf("bad response status: %s", resp.Status)
+	}
+
+	return io.Copy(w, resp.Body)
+}
+
+func (s Speco) Pipeline(ctx context.Context, w io.Writer, interval time.Duration) error {
+
+	for {
+		if ctx.Err() != nil {
+			return ctx.Err()
+		}
+
+		startTime := time.Now()
+		_, err := s.Snapshot(w)
+		if err != nil {
+			return err
+		}
+
+		elapsed := time.Since(startTime)
+		sleepTime := interval - elapsed
+		if sleepTime < 0 {
+			slog.Warn("late, no sleep",
+				slog.String("elapsed", elapsed.String()))
+		}
+		if sleepTime > 0 {
+			slog.Debug("sleeping",
+				slog.String("elapsed", elapsed.String()),
+				slog.String("sleeping", sleepTime.String()),
+			)
+			time.Sleep(sleepTime)
+		}
+	}
+}
+
+var ffmpegArgs = []string{
+	"-hide_banner",
+	"-loglevel", "warning", // logs
+	"-f", "image2pipe", "-framerate", "1", "-i", "pipe:0", // input from pipe
+	"-vf", "setpts=PTS/120,format=yuv420p", // compression 120x speed
+	"-r", "120", // 120fps output
+	"-c:v", "libx264", "-preset", "slow", "-crf", "25", // H.264 encoding settings
+	"-g", "600", "-keyint_min", "600", "-bf", "3", // keyframe and b-frame settings
+	"-movflags", "+faststart", // webplayback
+	"timelapse.mp4", // output file
+}
.gitignore
@@ -0,0 +1,2 @@
+*.jpg
+*.mp4
capture.go
@@ -1,69 +1,84 @@
 package main
 
 import (
+	"christmas-cam/internal/speco"
+	"context"
 	"fmt"
 	"io"
 	"log/slog"
-	"net/http"
 	"net/url"
 	"os"
+	"os/exec"
 	"time"
 )
 
 func main() {
-	snapshotURL := "http://USERNAME:PASSWORD@192.168.58.58/snapshot.JPG"
-	client := &http.Client{
-		Timeout: 1700 * time.Millisecond,
+
+	snapshotURLString := "http://admin:specospeco1!@192.168.58.58/snapshot.JPG"
+	snapshotURL, err := url.Parse(snapshotURLString)
+	if err != nil {
+		panic(err)
 	}
+	camera := speco.NewCamera(snapshotURL, 3*time.Second)
+	slog.SetLogLoggerLevel(slog.LevelDebug)
 
-	for {
-		startTime := time.Now()
+	ctx := context.TODO()
 
-		outputFile := fmt.Sprintf("%x.jpg", startTime.Unix())
+	// TODO ffmpeg starter func, returns io.WriteCloser error
+	cmd := exec.CommandContext(ctx, "ffmpeg", ffmpegArgs...)
+	ffmpegPipe, err := cmd.StdinPipe()
+	if err != nil {
+		panic(err)
+	}
+	cmd.Stderr = os.Stderr
+	cmd.Stdout = os.Stderr
+	err = cmd.Start()
+	if err != nil {
+		panic(err)
+	}
 
-		resp, err := client.Get(snapshotURL)
-		if err != nil {
-			if urlError, ok := err.(*url.Error); ok && urlError.Timeout() {
-				elapsed := time.Since(startTime)
-				slog.Warn("timed out",
-					slog.String("elapsed", elapsed.String()))
-				continue
-			} else {
-				slog.Error("failed to fetch URL",
-					slog.String("err", err.Error()))
-				continue
-			}
-		}
-		defer resp.Body.Close()
+	interval := 2 * time.Second
+	next := time.Now()
+	for {
 
-		if resp.StatusCode != http.StatusOK {
-			slog.Error("bad response status",
-				slog.String("status", resp.Status))
-		}
+		time.Sleep(time.Until(next))
+		next = next.Add(interval)
+		start := time.Now()
 
-		out, err := os.Create(outputFile)
+		filename := fmt.Sprintf("%x.jpg", start.Unix())
+		snapshotFile, err := os.Create(filename)
 		if err != nil {
 			slog.Error("failed to create file",
 				slog.String("err", err.Error()),
-				slog.String("file", outputFile))
+				slog.String("file", filename))
+			return
 		}
-		defer out.Close()
+		defer snapshotFile.Close()
 
-		_, err = io.Copy(out, resp.Body)
+		mw := io.MultiWriter(snapshotFile, ffmpegPipe)
+		_, err = camera.Snapshot(mw)
 		if err != nil {
 			slog.Error("failed to save image",
 				slog.String("err", err.Error()))
 			continue
 		}
 
-		elapsed := time.Since(startTime)
-		sleepTime := time.Second - elapsed
-		if sleepTime < 0 {
-			slog.Warn("late, no sleep",
+		elapsed := time.Since(start)
+		if elapsed > interval {
+			slog.Warn("snapshot overran interval",
 				slog.String("elapsed", elapsed.String()))
 		}
-		if sleepTime > 0 {
-			time.Sleep(sleepTime)
-		}
 	}
 }
+
+var ffmpegArgs = []string{
+	"-y",                // Overwrite output files without asking.
+	"-loglevel", "info", // logs
+	"-f", "mjpeg", "-framerate", "1", "-i", "pipe:0", // input from pipe
+	"-vf", "setpts=PTS/120,scale=in_range=jpeg:out_range=tv,format=yuv420p", // compression 120x speed
+	"-c:v", "libx264", "-preset", "slow", "-crf", "25", // H.264 encoding settings
+	"-g", "600", "-keyint_min", "600", "-bf", "3", // keyframe and b-frame settings
+	"-fps_mode", "passthrough", //
+	"-movflags", "+faststart", // webplayback
+	"timelapse.mp4", // output file
+}