Commit e5ee25e
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
+}