main
1package speco
2
3import (
4 "context"
5 "fmt"
6 "io"
7 "log/slog"
8 "net/http"
9 "net/url"
10 "time"
11)
12
13type Speco struct {
14 url *url.URL
15 client *http.Client
16}
17
18type JPEG []byte
19
20func NewCamera(
21 url *url.URL,
22 timeout time.Duration,
23) Speco {
24 return Speco{
25 url: url,
26 client: &http.Client{
27 Timeout: timeout,
28 },
29 }
30}
31
32func (s Speco) Snapshot(w io.Writer) (int64, error) {
33
34 resp, err := s.client.Get(s.url.String())
35 if err != nil {
36 if urlError, ok := err.(*url.Error); ok && urlError.Timeout() {
37 return 0, err
38 }
39 return 0, err
40 }
41 defer resp.Body.Close()
42
43 if resp.StatusCode != http.StatusOK {
44 return 0, fmt.Errorf("bad response status: %s", resp.Status)
45 }
46
47 return io.Copy(w, resp.Body)
48}
49
50func (s Speco) Pipeline(ctx context.Context, w io.Writer, interval time.Duration) error {
51
52 for {
53 if ctx.Err() != nil {
54 return ctx.Err()
55 }
56
57 startTime := time.Now()
58 _, err := s.Snapshot(w)
59 if err != nil {
60 return err
61 }
62
63 elapsed := time.Since(startTime)
64 sleepTime := interval - elapsed
65 if sleepTime < 0 {
66 slog.Warn("late, no sleep",
67 slog.String("elapsed", elapsed.String()))
68 }
69 if sleepTime > 0 {
70 slog.Debug("sleeping",
71 slog.String("elapsed", elapsed.String()),
72 slog.String("sleeping", sleepTime.String()),
73 )
74 time.Sleep(sleepTime)
75 }
76 }
77}
78
79var ffmpegArgs = []string{
80 "-hide_banner",
81 "-loglevel", "warning", // logs
82 "-f", "image2pipe", "-framerate", "1", "-i", "pipe:0", // input from pipe
83 "-vf", "setpts=PTS/120,format=yuv420p", // compression 120x speed
84 "-r", "120", // 120fps output
85 "-c:v", "libx264", "-preset", "slow", "-crf", "25", // H.264 encoding settings
86 "-g", "600", "-keyint_min", "600", "-bf", "3", // keyframe and b-frame settings
87 "-movflags", "+faststart", // webplayback
88 "timelapse.mp4", // output file
89}