Simple MJPEG streamer for ESPHome cameras.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

186 lines
3.9 KiB

package main
import (
"net/http"
"sort"
"strings"
"sync"
"time"
"maze.io/x/esphome"
"github.com/flosch/pongo2"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
)
// Server defaults.
var (
DefaultServerListen = "localhost:6080"
DefaultServerStatic = "static"
DefaultServerTemplate = "template"
)
type Server struct {
*echo.Echo
m *Manager
mutex sync.RWMutex
camera map[string]*Camera
config *Config
}
func NewServer(config *Config) (*Server, error) {
var (
s = &Server{
Echo: echo.New(),
m: NewManager(config),
camera: make(map[string]*Camera),
config: config,
}
err error
)
if s.Renderer, err = NewRenderer(config.Template); err != nil {
return nil, err
}
s.HideBanner = true
// HTTP middlewares
s.Use(middleware.AddTrailingSlash())
s.Use(Logger())
//s.Use(middleware.Logger())
s.Use(middleware.Recover())
s.Use(middleware.Static(config.Static))
// HTTP routes
s.GET("/", s.index)
s.GET("/device/:name", s.deviceIndex)
s.GET("/device/:name/camera/current.jpeg", s.deviceCameraImage)
s.GET("/device/:name/camera/stream.jpeg", s.deviceCameraStream)
go s.streamCameras()
return s, nil
}
func (s *Server) streamCameras() {
s.startCameras()
ticker := time.NewTicker(s.config.Scan.Interval)
for {
<-ticker.C
s.startCameras()
}
}
func (s *Server) startCameras() {
for name, device := range s.m.Device {
log := log.WithField("device", name)
s.mutex.RLock()
_, streaming := s.camera[name]
s.mutex.RUnlock()
if streaming {
continue
}
if device.IsAvailable() {
camera, err := device.client.Camera()
if err != nil {
if err != esphome.ErrEntity {
log.WithError(err).Warn("failed to start camera")
}
continue
}
log.Info("starting camera stream")
cameraStream, err := NewCamera(camera)
if err != nil {
log.WithError(err).Warn("failed to start camera stream")
continue
}
s.mutex.Lock()
s.camera[name] = cameraStream
device.Camera = cameraStream
s.mutex.Unlock()
}
}
}
func (s *Server) index(c echo.Context) error {
var devices = make([]*Device, 0, len(s.m.Device))
for _, device := range s.m.Device {
devices = append(devices, device)
}
sort.Slice(devices, func(i, j int) bool {
var a, b = devices[i], devices[j]
if a.Camera != nil && b.Camera == nil {
return true
}
if b.Camera != nil && a.Camera == nil {
return false
}
return strings.Compare(a.Name, b.Name) < 0
})
return c.Render(http.StatusOK, "index.html", pongo2.Context{
"devices": devices,
})
}
func (s *Server) deviceIndex(c echo.Context) error {
device, ok := s.m.Device[c.Param("name")]
if !ok {
return echo.NewHTTPError(http.StatusNotFound, "Device not found")
}
return c.Render(http.StatusOK, "device.html", pongo2.Context{
"device": device,
})
}
func (s *Server) deviceCameraImage(c echo.Context) error {
var (
name = c.Param("name")
response = c.Response()
header = response.Header()
camera, ok = s.camera[name]
)
if !ok {
log.WithField("camera", name).Debug("not found")
return echo.NewHTTPError(http.StatusNotFound, "Camera not found")
}
header.Set("Content-Type", "image/jpeg")
_, err := response.Write(camera.JPEG())
return err
}
func (s *Server) deviceCameraStream(c echo.Context) error {
var (
name = c.Param("name")
addr = c.Request().RemoteAddr
camera, ok = s.camera[name]
)
if !ok {
log.WithField("camera", name).Debug("not found")
return echo.NewHTTPError(http.StatusNotFound, "Camera not found")
}
stream := make(chan []byte)
camera.Add(stream)
defer camera.Remove(stream)
log.Printf("streamer: camera %s has new client %s", name, addr)
w := c.Response().Writer
w.Header().Add("Content-Type", "multipart/x-mixed-replace;boundary="+boundaryWord)
for frame := range stream {
if _, err := w.Write(frame); err != nil {
return nil
}
if f, ok := w.(http.Flusher); ok {
f.Flush()
}
}
return nil
}