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.
 
 

94 lines
1.6 KiB

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
}