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

2 years ago
  1. package main
  2. import (
  3. "net/http"
  4. "sort"
  5. "strings"
  6. "sync"
  7. "time"
  8. "maze.io/x/esphome"
  9. "github.com/flosch/pongo2"
  10. "github.com/labstack/echo/v4"
  11. "github.com/labstack/echo/v4/middleware"
  12. )
  13. // Server defaults.
  14. var (
  15. DefaultServerListen = "localhost:6080"
  16. DefaultServerStatic = "static"
  17. DefaultServerTemplate = "template"
  18. )
  19. type Server struct {
  20. *echo.Echo
  21. m *Manager
  22. mutex sync.RWMutex
  23. camera map[string]*Camera
  24. config *Config
  25. }
  26. func NewServer(config *Config) (*Server, error) {
  27. var (
  28. s = &Server{
  29. Echo: echo.New(),
  30. m: NewManager(config),
  31. camera: make(map[string]*Camera),
  32. config: config,
  33. }
  34. err error
  35. )
  36. if s.Renderer, err = NewRenderer(config.Template); err != nil {
  37. return nil, err
  38. }
  39. s.HideBanner = true
  40. // HTTP middlewares
  41. s.Use(middleware.AddTrailingSlash())
  42. s.Use(Logger())
  43. //s.Use(middleware.Logger())
  44. s.Use(middleware.Recover())
  45. s.Use(middleware.Static(config.Static))
  46. // HTTP routes
  47. s.GET("/", s.index)
  48. s.GET("/device/:name", s.deviceIndex)
  49. s.GET("/device/:name/camera/current.jpeg", s.deviceCameraImage)
  50. s.GET("/device/:name/camera/stream.jpeg", s.deviceCameraStream)
  51. go s.streamCameras()
  52. return s, nil
  53. }
  54. func (s *Server) streamCameras() {
  55. s.startCameras()
  56. ticker := time.NewTicker(s.config.Scan.Interval)
  57. for {
  58. <-ticker.C
  59. s.startCameras()
  60. }
  61. }
  62. func (s *Server) startCameras() {
  63. for name, device := range s.m.Device {
  64. log := log.WithField("device", name)
  65. s.mutex.RLock()
  66. _, streaming := s.camera[name]
  67. s.mutex.RUnlock()
  68. if streaming {
  69. continue
  70. }
  71. if device.IsAvailable() {
  72. camera, err := device.client.Camera()
  73. if err != nil {
  74. if err != esphome.ErrEntity {
  75. log.WithError(err).Warn("failed to start camera")
  76. }
  77. continue
  78. }
  79. log.Info("starting camera stream")
  80. cameraStream, err := NewCamera(camera)
  81. if err != nil {
  82. log.WithError(err).Warn("failed to start camera stream")
  83. continue
  84. }
  85. s.mutex.Lock()
  86. s.camera[name] = cameraStream
  87. device.Camera = cameraStream
  88. s.mutex.Unlock()
  89. }
  90. }
  91. }
  92. func (s *Server) index(c echo.Context) error {
  93. var devices = make([]*Device, 0, len(s.m.Device))
  94. for _, device := range s.m.Device {
  95. devices = append(devices, device)
  96. }
  97. sort.Slice(devices, func(i, j int) bool {
  98. var a, b = devices[i], devices[j]
  99. if a.Camera != nil && b.Camera == nil {
  100. return true
  101. }
  102. if b.Camera != nil && a.Camera == nil {
  103. return false
  104. }
  105. return strings.Compare(a.Name, b.Name) < 0
  106. })
  107. return c.Render(http.StatusOK, "index.html", pongo2.Context{
  108. "devices": devices,
  109. })
  110. }
  111. func (s *Server) deviceIndex(c echo.Context) error {
  112. device, ok := s.m.Device[c.Param("name")]
  113. if !ok {
  114. return echo.NewHTTPError(http.StatusNotFound, "Device not found")
  115. }
  116. return c.Render(http.StatusOK, "device.html", pongo2.Context{
  117. "device": device,
  118. })
  119. }
  120. func (s *Server) deviceCameraImage(c echo.Context) error {
  121. var (
  122. name = c.Param("name")
  123. response = c.Response()
  124. header = response.Header()
  125. camera, ok = s.camera[name]
  126. )
  127. if !ok {
  128. log.WithField("camera", name).Debug("not found")
  129. return echo.NewHTTPError(http.StatusNotFound, "Camera not found")
  130. }
  131. header.Set("Content-Type", "image/jpeg")
  132. _, err := response.Write(camera.JPEG())
  133. return err
  134. }
  135. func (s *Server) deviceCameraStream(c echo.Context) error {
  136. var (
  137. name = c.Param("name")
  138. addr = c.Request().RemoteAddr
  139. camera, ok = s.camera[name]
  140. )
  141. if !ok {
  142. log.WithField("camera", name).Debug("not found")
  143. return echo.NewHTTPError(http.StatusNotFound, "Camera not found")
  144. }
  145. stream := make(chan []byte)
  146. camera.Add(stream)
  147. defer camera.Remove(stream)
  148. log.Printf("streamer: camera %s has new client %s", name, addr)
  149. w := c.Response().Writer
  150. w.Header().Add("Content-Type", "multipart/x-mixed-replace;boundary="+boundaryWord)
  151. for frame := range stream {
  152. if _, err := w.Write(frame); err != nil {
  153. return nil
  154. }
  155. if f, ok := w.(http.Flusher); ok {
  156. f.Flush()
  157. }
  158. }
  159. return nil
  160. }