main
Raw Download raw file
 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}