diff --git a/README.md b/README.md index 6956fcc..4a88744 100644 --- a/README.md +++ b/README.md @@ -65,6 +65,8 @@ Usage: cam2ip [] Bind address [CAM2IP_BIND_ADDR] (default ":56000") --htpasswd-file Path to htpasswd file, if empty auth is disabled [CAM2IP_HTPASSWD_FILE] (default "") + --lazy + Open the camera only while clients are connected [CAM2IP_LAZY] (default "false") --list-devices List available cameras and exit (default "false") --version diff --git a/cmd/cam2ip/main.go b/cmd/cam2ip/main.go index 160cf90..14939b2 100644 --- a/cmd/cam2ip/main.go +++ b/cmd/cam2ip/main.go @@ -10,6 +10,7 @@ import ( "go.senan.xyz/flagconf" "github.com/gen2brain/cam2ip/camera" + "github.com/gen2brain/cam2ip/handlers" "github.com/gen2brain/cam2ip/server" ) @@ -57,6 +58,7 @@ func main() { flag.StringVar(&srv.TimeFormat, "time-format", "2006-01-02 15:04:05", "Time format [CAM2IP_TIME_FORMAT]") flag.StringVar(&srv.Bind, "bind-addr", ":56000", "Bind address [CAM2IP_BIND_ADDR]") flag.StringVar(&srv.Htpasswd, "htpasswd-file", "", "Path to htpasswd file, if empty auth is disabled [CAM2IP_HTPASSWD_FILE]") + flag.BoolVar(&srv.Lazy, "lazy", false, "Open the camera only while clients are connected [CAM2IP_LAZY]") var showVersion bool flag.BoolVar(&showVersion, "version", false, "Print version and exit") @@ -69,7 +71,7 @@ func main() { stderr("%s %s []\n", colorize(color, colorBold, "Usage:"), name) order := []string{"index", "device", "delay", "width", "height", "quality", "rotate", "flip", "no-webgl", - "timestamp", "time-format", "bind-addr", "htpasswd-file", "list-devices", "version"} + "timestamp", "time-format", "bind-addr", "htpasswd-file", "lazy", "list-devices", "version"} for _, name := range order { f := flag.Lookup(name) @@ -121,7 +123,7 @@ func main() { } } - cam, err := camera.New(camera.Options{ + opts := camera.Options{ Index: srv.Index, Rotate: srv.Rotate, Flip: srv.Flip, @@ -129,26 +131,37 @@ func main() { Height: srv.Height, Timestamp: srv.Timestamp, TimeFormat: srv.TimeFormat, - }) - if err != nil { - stderr("%s\n", err.Error()) - os.Exit(1) } - srv.Reader = cam - - defer srv.Reader.Close() - - info := cam.Info() - desc := fmt.Sprintf("%dx%d %s", info.Width, info.Height, info.Format) - if dn := deviceName(srv.Index); dn != "" { - desc = dn + ", " + desc + srv.Open = func() (handlers.ImageReader, error) { + return camera.New(opts) } - stderr("%s %s [%s] listening on %s\n", name, version, desc, srv.Bind) + if srv.Lazy { + stderr("%s %s [lazy] listening on %s\n", name, version, srv.Bind) + } else { + cam, err := camera.New(opts) + if err != nil { + stderr("%s\n", err.Error()) + os.Exit(1) + } - err = srv.ListenAndServe() - if err != nil { + defer cam.Close() + + srv.Open = func() (handlers.ImageReader, error) { + return cam, nil + } + + info := cam.Info() + desc := fmt.Sprintf("%dx%d %s", info.Width, info.Height, info.Format) + if dn := deviceName(srv.Index); dn != "" { + desc = dn + ", " + desc + } + + stderr("%s %s [%s] listening on %s\n", name, version, desc, srv.Bind) + } + + if err := srv.ListenAndServe(); err != nil { stderr("%s\n", err.Error()) os.Exit(1) } diff --git a/handlers/stream.go b/handlers/stream.go index d81b212..c2806e4 100644 --- a/handlers/stream.go +++ b/handlers/stream.go @@ -13,25 +13,33 @@ const errorBackoff = 100 * time.Millisecond // Stream captures frames in a single loop and broadcasts the encoded JPEG to all subscribers. type Stream struct { - reader ImageReader + open func() (ImageReader, error) + lazy bool delay int quality int + reader ImageReader + mu sync.Mutex cond *sync.Cond subs map[chan []byte]struct{} } -// NewStream returns a new Stream and starts its capture loop. -func NewStream(reader ImageReader, delay, quality int) *Stream { +// NewStream returns a new Stream and starts its capture loop. open acquires the camera: once up front, or while clients are connected when lazy. +func NewStream(open func() (ImageReader, error), delay, quality int, lazy bool) *Stream { s := &Stream{ - reader: reader, + open: open, + lazy: lazy, delay: delay, quality: quality, subs: make(map[chan []byte]struct{}), } s.cond = sync.NewCond(&s.mu) + if !lazy { + s.reader, _ = open() + } + go s.capture() return s @@ -42,10 +50,27 @@ func (s *Stream) capture() { for { s.mu.Lock() for len(s.subs) == 0 { + if s.lazy && s.reader != nil { + s.reader.Close() + s.reader = nil + } + s.cond.Wait() } s.mu.Unlock() + if s.reader == nil { + reader, err := s.open() + if err != nil { + log.Printf("stream: open: %v", err) + time.Sleep(errorBackoff) + + continue + } + + s.reader = reader + } + img, err := s.reader.Read() if err != nil { log.Printf("stream: read: %v", err) diff --git a/server/server.go b/server/server.go index 4017986..f780453 100644 --- a/server/server.go +++ b/server/server.go @@ -36,7 +36,8 @@ type Server struct { Bind string Htpasswd string - Reader handlers.ImageReader + Lazy bool + Open func() (handlers.ImageReader, error) } // NewServer returns new Server. @@ -54,7 +55,7 @@ func (s *Server) ListenAndServe() error { basic = auth.NewBasicAuthenticator(realm, auth.HtpasswdFileProvider(s.Htpasswd)) } - stream := handlers.NewStream(s.Reader, s.Delay, s.Quality) + stream := handlers.NewStream(s.Open, s.Delay, s.Quality, s.Lazy) mux := http.NewServeMux()