install go2rtc on bob

This commit is contained in:
2026-04-04 19:36:14 +02:00
parent f0b56e63d1
commit ccf88187b8
537 changed files with 69213 additions and 0 deletions
@@ -0,0 +1,108 @@
# Motion JPEG
- This module can provide and receive streams in MJPEG format.
- This module is also responsible for receiving snapshots in JPEG format.
- This module also supports streaming to the server console (terminal) in the **animated ASCII art** format.
## MJPEG Client
**Important.** For a stream in MJPEG format, your source MUST contain the MJPEG codec. If your stream has the MJPEG codec, you can receive an **MJPEG stream** or **JPEG snapshots** via the API.
You can receive an MJPEG stream in several ways:
- some cameras support MJPEG codec inside [RTSP stream](../rtsp/README.md) (ex. second stream for Dahua cameras)
- some cameras have an HTTP link with [MJPEG stream](../http/README.md)
- some cameras have an HTTP link with snapshots - go2rtc can convert them to [MJPEG stream](../http/README.md)
- you can convert an H264/H265 stream from your camera via [FFmpeg integration](../ffmpeg/README.md)
With this example, your stream will have both H264 and MJPEG codecs:
```yaml
streams:
camera1:
- rtsp://rtsp:12345678@192.168.1.123/av_stream/ch0
- ffmpeg:camera1#video=mjpeg
```
## MJPEG Server
### mpjpeg
Output a stream in [MJPEG](https://en.wikipedia.org/wiki/Motion_JPEG) format. In [FFmpeg](https://ffmpeg.org/), this format is called `mpjpeg` because it contains HTTP headers.
```
ffplay http://192.168.1.123:1984/api/stream.mjpeg?src=camera1
```
### jpeg
Receiving a JPEG snapshot.
```
curl http://192.168.1.123:1984/api/frame.jpeg?src=camera1
```
- You can use `width`/`w` and/or `height`/`h` parameters.
- You can use `rotate` param with `90`, `180`, `270` or `-90` values.
- You can use `hardware`/`hw` param [read more](https://github.com/AlexxIT/go2rtc/wiki/Hardware-acceleration).
- You can use `cache` param (`1m`, `10s`, etc.) to get a cached snapshot.
- The snapshot is cached only when requested with the `cache` parameter.
- A cached snapshot will be used if its time is not older than the time specified in the `cache` parameter.
- The `cache` parameter does not check the image dimensions from the cache and those specified in the query.
### ascii
Stream as ASCII to Terminal. This format is just for fun. You can boast to your friends that you can stream cameras even to the server console without a GUI.
[![](https://img.youtube.com/vi/sHj_3h_sX7M/mqdefault.jpg)](https://www.youtube.com/watch?v=sHj_3h_sX7M)
> The demo video features a combination of several settings for this format with added audio. Of course, the format doesn't support audio out of the box.
**Tips**
- this feature works only with MJPEG codec (use transcoding)
- choose a low frame rate (FPS)
- choose the width and height to fit in your terminal
- different terminals support different numbers of colors (8, 256, rgb)
- URL-encode the `text` parameter
- you can stream any camera or file from disk
**go2rtc.yaml** - transcoding to MJPEG, terminal size - 210x59 (16/9), fps - 10
```yaml
streams:
gamazda: ffmpeg:gamazda.mp4#video=mjpeg#hardware#width=210#height=59#raw=-r 10
```
**API params**
- `color` - foreground color, values: empty, `8`, `256`, `rgb`, [SGR](https://en.wikipedia.org/wiki/ANSI_escape_code)
- example: `30` (black), `37` (white), `38;5;226` (yellow)
- `back` - background color, values: empty, `8`, `256`, `rgb`, [SGR](https://en.wikipedia.org/wiki/ANSI_escape_code)
- example: `40` (black), `47` (white), `48;5;226` (yellow)
- `text` - character set, values: empty, one character, `block`, list of chars (in order of brightness)
- example: `%20` (space), `block` (keyword for block elements), `ox` (two chars)
**Examples**
```bash
% curl "http://192.168.1.123:1984/api/stream.ascii?src=gamazda"
% curl "http://192.168.1.123:1984/api/stream.ascii?src=gamazda&color=256"
% curl "http://192.168.1.123:1984/api/stream.ascii?src=gamazda&back=256&text=%20"
% curl "http://192.168.1.123:1984/api/stream.ascii?src=gamazda&back=8&text=%20%20"
% curl "http://192.168.1.123:1984/api/stream.ascii?src=gamazda&text=helloworld"
```
### yuv4mpegpipe
Raw [YUV](https://en.wikipedia.org/wiki/Y%E2%80%B2UV) frame stream with [YUV4MPEG](https://manned.org/yuv4mpeg) header.
```
ffplay http://192.168.1.123:1984/api/stream.y4m?src=camera1
```
## Streaming ingest
```shell
ffmpeg -re -i BigBuckBunny.mp4 -c mjpeg -f mpjpeg http://localhost:1984/api/stream.mjpeg?dst=camera1
```
@@ -0,0 +1,237 @@
package mjpeg
import (
"errors"
"io"
"net/http"
"strconv"
"strings"
"sync"
"time"
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/internal/api/ws"
"github.com/AlexxIT/go2rtc/internal/app"
"github.com/AlexxIT/go2rtc/internal/ffmpeg"
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/pkg/ascii"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/magic"
"github.com/AlexxIT/go2rtc/pkg/mjpeg"
"github.com/AlexxIT/go2rtc/pkg/mpjpeg"
"github.com/AlexxIT/go2rtc/pkg/y4m"
"github.com/rs/zerolog"
)
func Init() {
api.HandleFunc("api/frame.jpeg", handlerKeyframe)
api.HandleFunc("api/stream.mjpeg", handlerStream)
api.HandleFunc("api/stream.ascii", handlerStream)
api.HandleFunc("api/stream.y4m", apiStreamY4M)
ws.HandleFunc("mjpeg", handlerWS)
log = app.GetLogger("mjpeg")
}
var log zerolog.Logger
func handlerKeyframe(w http.ResponseWriter, r *http.Request) {
query := r.URL.Query()
stream, _ := streams.GetOrPatch(query)
if stream == nil {
http.Error(w, api.StreamNotFound, http.StatusNotFound)
return
}
var b []byte
if s := query.Get("cache"); s != "" {
if timeout, err := time.ParseDuration(s); err == nil {
src := query.Get("src")
cacheMu.Lock()
entry, found := cache[src]
cacheMu.Unlock()
if found && time.Since(entry.timestamp) < timeout {
writeJPEGResponse(w, entry.payload)
return
}
defer func() {
if b == nil {
return
}
entry = cacheEntry{payload: b, timestamp: time.Now()}
cacheMu.Lock()
if cache == nil {
cache = map[string]cacheEntry{src: entry}
} else {
cache[src] = entry
}
cacheMu.Unlock()
}()
}
}
cons := magic.NewKeyframe()
cons.WithRequest(r)
if err := stream.AddConsumer(cons); err != nil {
log.Error().Err(err).Caller().Send()
return
}
once := &core.OnceBuffer{} // init and first frame
_, _ = cons.WriteTo(once)
b = once.Buffer()
stream.RemoveConsumer(cons)
switch cons.CodecName() {
case core.CodecH264, core.CodecH265:
ts := time.Now()
var err error
if b, err = ffmpeg.JPEGWithQuery(b, query); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
log.Debug().Msgf("[mjpeg] transcoding time=%s", time.Since(ts))
case core.CodecJPEG:
b = mjpeg.FixJPEG(b)
}
writeJPEGResponse(w, b)
}
var cache map[string]cacheEntry
var cacheMu sync.Mutex
// cacheEntry represents a cached keyframe with its timestamp
type cacheEntry struct {
payload []byte
timestamp time.Time
}
func writeJPEGResponse(w http.ResponseWriter, b []byte) {
h := w.Header()
h.Set("Content-Type", "image/jpeg")
h.Set("Content-Length", strconv.Itoa(len(b)))
h.Set("Cache-Control", "no-cache")
h.Set("Connection", "close")
h.Set("Pragma", "no-cache")
if _, err := w.Write(b); err != nil {
log.Error().Err(err).Caller().Send()
}
}
func handlerStream(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
outputMjpeg(w, r)
} else {
inputMjpeg(w, r)
}
}
func outputMjpeg(w http.ResponseWriter, r *http.Request) {
src := r.URL.Query().Get("src")
stream := streams.Get(src)
if stream == nil {
http.Error(w, api.StreamNotFound, http.StatusNotFound)
return
}
cons := mjpeg.NewConsumer()
cons.WithRequest(r)
if err := stream.AddConsumer(cons); err != nil {
log.Error().Err(err).Msg("[api.mjpeg] add consumer")
return
}
h := w.Header()
h.Set("Cache-Control", "no-cache")
h.Set("Connection", "close")
h.Set("Pragma", "no-cache")
if strings.HasSuffix(r.URL.Path, "mjpeg") {
wr := mjpeg.NewWriter(w)
_, _ = cons.WriteTo(wr)
} else {
cons.FormatName = "ascii"
query := r.URL.Query()
wr := ascii.NewWriter(w, query.Get("color"), query.Get("back"), query.Get("text"))
_, _ = cons.WriteTo(wr)
}
stream.RemoveConsumer(cons)
}
func inputMjpeg(w http.ResponseWriter, r *http.Request) {
dst := r.URL.Query().Get("dst")
stream := streams.Get(dst)
if stream == nil {
http.Error(w, api.StreamNotFound, http.StatusNotFound)
return
}
prod, _ := mpjpeg.Open(r.Body)
prod.WithRequest(r)
stream.AddProducer(prod)
if err := prod.Start(); err != nil && err != io.EOF {
log.Warn().Err(err).Caller().Send()
}
stream.RemoveProducer(prod)
}
func handlerWS(tr *ws.Transport, _ *ws.Message) error {
stream, _ := streams.GetOrPatch(tr.Request.URL.Query())
if stream == nil {
return errors.New(api.StreamNotFound)
}
cons := mjpeg.NewConsumer()
cons.WithRequest(tr.Request)
if err := stream.AddConsumer(cons); err != nil {
log.Debug().Err(err).Msg("[mjpeg] add consumer")
return err
}
tr.Write(&ws.Message{Type: "mjpeg"})
go cons.WriteTo(tr.Writer())
tr.OnClose(func() {
stream.RemoveConsumer(cons)
})
return nil
}
func apiStreamY4M(w http.ResponseWriter, r *http.Request) {
src := r.URL.Query().Get("src")
stream := streams.Get(src)
if stream == nil {
http.Error(w, api.StreamNotFound, http.StatusNotFound)
return
}
cons := y4m.NewConsumer()
cons.WithRequest(r)
if err := stream.AddConsumer(cons); err != nil {
log.Error().Err(err).Caller().Send()
return
}
_, _ = cons.WriteTo(w)
stream.RemoveConsumer(cons)
}