package main
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"sync"
|
|
|
|
"maze.io/x/esphome"
|
|
)
|
|
|
|
// Camera stream.
|
|
type Camera struct {
|
|
esphome.Camera
|
|
sync.RWMutex
|
|
jpeg []byte
|
|
frame []byte
|
|
client map[chan []byte]struct{}
|
|
}
|
|
|
|
// NewCamera starts streaming from the ESPHome camera.
|
|
func NewCamera(camera esphome.Camera) (*Camera, error) {
|
|
c := &Camera{
|
|
Camera: camera,
|
|
client: make(map[chan []byte]struct{}),
|
|
}
|
|
|
|
stream, err := c.Stream()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
go c.stream(stream)
|
|
|
|
return c, nil
|
|
}
|
|
|
|
const boundaryWord = "MJPEGBOUNDARY"
|
|
|
|
func (c *Camera) stream(stream <-chan *bytes.Buffer) {
|
|
for frame := range stream {
|
|
const headerFormat = "\r\n" +
|
|
"--" + boundaryWord + "\r\n" +
|
|
"Content-Type: image/jpeg\r\n" +
|
|
"Content-Length: %d\r\n" +
|
|
"X-Timestamp: 0.000000\r\n" +
|
|
"\r\n"
|
|
|
|
var (
|
|
jpeg = frame.Bytes()
|
|
size = len(jpeg)
|
|
header = fmt.Sprintf(headerFormat, size)
|
|
)
|
|
|
|
c.Lock()
|
|
if len(c.jpeg) < size {
|
|
c.jpeg = make([]byte, size)
|
|
}
|
|
copy(c.jpeg, jpeg)
|
|
if len(c.frame) < size+len(header) {
|
|
c.frame = make([]byte, (size+len(header))*2)
|
|
}
|
|
copy(c.frame, header)
|
|
copy(c.frame[len(header):], jpeg)
|
|
for client := range c.client {
|
|
select {
|
|
case client <- c.frame:
|
|
default:
|
|
}
|
|
}
|
|
c.Unlock()
|
|
}
|
|
}
|
|
|
|
// Add a client stream.
|
|
func (c *Camera) Add(client chan []byte) {
|
|
c.Lock()
|
|
c.client[client] = struct{}{}
|
|
c.Unlock()
|
|
}
|
|
|
|
// Remove a client stream.
|
|
func (c *Camera) Remove(client chan []byte) {
|
|
c.Lock()
|
|
delete(c.client, client)
|
|
c.Unlock()
|
|
}
|
|
|
|
// JPEG returns the last received raw JPEG.
|
|
func (c *Camera) JPEG() []byte {
|
|
c.RLock()
|
|
jpeg := make([]byte, len(c.jpeg))
|
|
copy(jpeg, c.jpeg)
|
|
c.RUnlock()
|
|
return jpeg
|
|
}
|